diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/CipherFactory.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/CipherFactory.java
new file mode 100644
index 0000000..2e82ac7
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/CipherFactory.java
@@ -0,0 +1,44 @@
+package org.apache.lucene.codecs.encrypted;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import javax.crypto.Cipher;
+
+public interface CipherFactory {
+
+  /** Attribute key for cipher version in segment info. */
+  String CIPHER_VERSION_KEY = CipherFactory.class.getSimpleName() + ".version";
+
+  /** Attribute key for encrypted field in field info. */
+  String ENCRYPTED_FIELD_KEY = CipherFactory.class.getSimpleName() + ".encrypted";
+
+  Cipher newEncipherInstance();
+
+  long getEncipherVersion();
+
+  Cipher newDecipherInstance(long version);
+
+  final class CipherFactoryException extends RuntimeException {
+
+    protected CipherFactoryException(String message, Throwable cause) {
+      super(message, cause);
+    }
+
+  }
+
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/DummyCipherFactory.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/DummyCipherFactory.java
new file mode 100644
index 0000000..fd34b80
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/DummyCipherFactory.java
@@ -0,0 +1,76 @@
+package org.apache.lucene.codecs.encrypted;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * An implementation example of the {@link CipherFactory} that uses a fixed key and initialisation vector.
+ */
+public class DummyCipherFactory implements CipherFactory {
+
+  public static byte[] getAESKey() {
+    return new byte[]{-107, 23, 92, 118, -64, -72, -22, 6, -126, 77, 52, 104, 65, 27, -98, -91,
+        -51, 36, -93, 21, -12, -50, 116, 14, -97, -47, -128, 80, -114, -6, -59, 39};
+  }
+
+  public static byte[] getIVBytes() {
+    return new byte[]{58, -26, 73, 35, -51, 111, 17, -77, -39, 118, 113, -6, 84, -22, -95, 11};
+  }
+
+  @Override
+  public Cipher newEncipherInstance() {
+    try {
+      //AESKey
+      SecretKeySpec aesKeySpec = new SecretKeySpec(getAESKey(), "AES");
+      //Initialization Vector
+      IvParameterSpec ips = new IvParameterSpec(getIVBytes());
+      //Cipher
+      Cipher aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+      aesCipher.init(Cipher.ENCRYPT_MODE, aesKeySpec, ips);
+      return aesCipher;
+    } catch (Exception e) {
+      throw new CipherFactoryException("Cannot instantiate encipher instance", e);
+    }
+  }
+
+  @Override
+  public long getEncipherVersion() {
+    return 0;
+  }
+
+  @Override
+  public Cipher newDecipherInstance(long version) {
+    assert version == 0;
+    try {
+      //AESKey
+      SecretKeySpec aesKeySpec = new SecretKeySpec(getAESKey(), "AES");
+      //Initialization Vector
+      IvParameterSpec ips = new IvParameterSpec(getIVBytes());
+      //Cipher
+      Cipher aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+      aesCipher.init(Cipher.DECRYPT_MODE, aesKeySpec, ips);
+      return aesCipher;
+    } catch (Exception e) {
+      throw new CipherFactoryException("Cannot instantiate decipher instance", e);
+    }
+  }
+
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/DummyEncryptedLucene60Codec.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/DummyEncryptedLucene60Codec.java
new file mode 100644
index 0000000..3344450
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/DummyEncryptedLucene60Codec.java
@@ -0,0 +1,68 @@
+package org.apache.lucene.codecs.encrypted;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.PostingsFormat;
+import org.apache.lucene.codecs.StoredFieldsFormat;
+import org.apache.lucene.codecs.compressing.CompressionMode;
+import org.apache.lucene.codecs.encrypted.compressing.EncryptedCompressingStoredFieldsFormat;
+
+/**
+ * An implementation example of the {@link EncryptedLucene60Codec} based on the {@link DummyCipherFactory} that encrypts
+ * all the fields. It can be used to run the full Lucene test suite against the {@link EncryptedLucene60Codec}.
+ */
+public class DummyEncryptedLucene60Codec extends EncryptedLucene60Codec {
+
+  @Override
+  public CipherFactory getCipherFactory() {
+    return new DummyCipherFactory();
+  }
+
+  @Override
+  public EncryptedLucene50StoredFieldsFormat storedFieldsFormat() {
+    return new EncryptedLucene50StoredFieldsFormat() {
+      @Override
+      protected CipherFactory getCipherFactory() {
+        return new DummyCipherFactory();
+      }
+
+      @Override
+      protected StoredFieldsFormat newEncryptedCompressingStoredFieldsFormat(String formatName, CipherFactory cipherFactory, CompressionMode compressionMode, int chunkSize, int maxDocsPerChunk, int blockSize) {
+        return new EncryptedCompressingStoredFieldsFormat(formatName, cipherFactory, compressionMode, chunkSize, maxDocsPerChunk, blockSize) {
+          @Override
+          public boolean isFieldEncrypted(String field) {
+            return true;
+          }
+        };
+      }
+    };
+  }
+
+  public static final class DummyEncryptedLucene50PostingsFormat extends EncryptedLucene50PostingsFormat {
+    @Override
+    protected CipherFactory getCipherFactory() {
+      return new DummyCipherFactory();
+    }
+  }
+
+  @Override
+  public PostingsFormat getPostingsFormatForField(String field) {
+    return new DummyEncryptedLucene50PostingsFormat();
+  }
+
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/EncryptedLucene50PostingsFormat.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/EncryptedLucene50PostingsFormat.java
new file mode 100644
index 0000000..ddedf82
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/EncryptedLucene50PostingsFormat.java
@@ -0,0 +1,452 @@
+package org.apache.lucene.codecs.encrypted;
+
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.CodecUtil;
+import org.apache.lucene.codecs.FieldsConsumer;
+import org.apache.lucene.codecs.FieldsProducer;
+import org.apache.lucene.codecs.MultiLevelSkipListWriter;
+import org.apache.lucene.codecs.PostingsFormat;
+import org.apache.lucene.codecs.PostingsReaderBase;
+import org.apache.lucene.codecs.PostingsWriterBase;
+import org.apache.lucene.codecs.blocktree.BlockTreeTermsWriter;
+import org.apache.lucene.codecs.encrypted.blocktree.EncryptedBlockTreeTermsReader;
+import org.apache.lucene.codecs.encrypted.blocktree.EncryptedBlockTreeTermsWriter;
+import org.apache.lucene.codecs.lucene50.Lucene50PostingsReader;
+import org.apache.lucene.codecs.lucene50.Lucene50PostingsWriter;
+import org.apache.lucene.index.IndexOptions;
+import org.apache.lucene.index.PostingsEnum;
+import org.apache.lucene.index.SegmentInfo;
+import org.apache.lucene.index.SegmentReadState;
+import org.apache.lucene.index.SegmentWriteState;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.util.IOUtils;
+import org.apache.lucene.util.packed.PackedInts;
+
+import java.io.IOException;
+
+/**
+ * Lucene 5.0 postings format, which encodes postings in packed integer blocks 
+ * for fast decode.
+ *
+ * <p>
+ * Basic idea:
+ * <ul>
+ *   <li>
+ *   <b>Packed Blocks and VInt Blocks</b>: 
+ *   <p>In packed blocks, integers are encoded with the same bit width ({@link PackedInts packed format}):
+ *      the block size (i.e. number of integers inside block) is fixed (currently 128). Additionally blocks
+ *      that are all the same value are encoded in an optimized way.</p>
+ *   <p>In VInt blocks, integers are encoded as {@link DataOutput#writeVInt VInt}:
+ *      the block size is variable.</p>
+ *   </li>
+ *
+ *   <li> 
+ *   <b>Block structure</b>: 
+ *   <p>When the postings are long enough, Lucene50PostingsFormat will try to encode most integer data 
+ *      as a packed block.</p> 
+ *   <p>Take a term with 259 documents as an example, the first 256 document ids are encoded as two packed 
+ *      blocks, while the remaining 3 are encoded as one VInt block. </p>
+ *   <p>Different kinds of data are always encoded separately into different packed blocks, but may 
+ *      possibly be interleaved into the same VInt block. </p>
+ *   <p>This strategy is applied to pairs: 
+ *      &lt;document number, frequency&gt;,
+ *      &lt;position, payload length&gt;, 
+ *      &lt;position, offset start, offset length&gt;, and
+ *      &lt;position, payload length, offsetstart, offset length&gt;.</p>
+ *   </li>
+ *
+ *   <li>
+ *   <b>Skipdata settings</b>: 
+ *   <p>The structure of skip table is quite similar to previous version of Lucene. Skip interval is the 
+ *      same as block size, and each skip entry points to the beginning of each block. However, for 
+ *      the first block, skip data is omitted.</p>
+ *   </li>
+ *
+ *   <li>
+ *   <b>Positions, Payloads, and Offsets</b>: 
+ *   <p>A position is an integer indicating where the term occurs within one document. 
+ *      A payload is a blob of metadata associated with current position. 
+ *      An offset is a pair of integers indicating the tokenized start/end offsets for given term 
+ *      in current position: it is essentially a specialized payload. </p>
+ *   <p>When payloads and offsets are not omitted, numPositions==numPayloads==numOffsets (assuming a 
+ *      null payload contributes one count). As mentioned in block structure, it is possible to encode 
+ *      these three either combined or separately. 
+ *   <p>In all cases, payloads and offsets are stored together. When encoded as a packed block, 
+ *      position data is separated out as .pos, while payloads and offsets are encoded in .pay (payload 
+ *      metadata will also be stored directly in .pay). When encoded as VInt blocks, all these three are 
+ *      stored interleaved into the .pos (so is payload metadata).</p>
+ *   <p>With this strategy, the majority of payload and offset data will be outside .pos file. 
+ *      So for queries that require only position data, running on a full index with payloads and offsets, 
+ *      this reduces disk pre-fetches.</p>
+ *   </li>
+ * </ul>
+ *
+ * <p>
+ * Files and detailed format:
+ * <ul>
+ *   <li><tt>.tim</tt>: <a href="#Termdictionary">Term Dictionary</a></li>
+ *   <li><tt>.tip</tt>: <a href="#Termindex">Term Index</a></li>
+ *   <li><tt>.doc</tt>: <a href="#Frequencies">Frequencies and Skip Data</a></li>
+ *   <li><tt>.pos</tt>: <a href="#Positions">Positions</a></li>
+ *   <li><tt>.pay</tt>: <a href="#Payloads">Payloads and Offsets</a></li>
+ * </ul>
+ *
+ * <a name="Termdictionary"></a>
+ * <dl>
+ * <dd>
+ * <b>Term Dictionary</b>
+ *
+ * <p>The .tim file contains the list of terms in each
+ * field along with per-term statistics (such as docfreq)
+ * and pointers to the frequencies, positions, payload and
+ * skip data in the .doc, .pos, and .pay files.
+ * See {@link BlockTreeTermsWriter} for more details on the format.
+ *
+ * <p>NOTE: The term dictionary can plug into different postings implementations:
+ * the postings writer/reader are actually responsible for encoding 
+ * and decoding the PostingsHeader and TermMetadata sections described here:
+ *
+ * <ul>
+ *   <li>PostingsHeader --&gt; Header, PackedBlockSize</li>
+ *   <li>TermMetadata --&gt; (DocFPDelta|SingletonDocID), PosFPDelta?, PosVIntBlockFPDelta?, PayFPDelta?, 
+ *                            SkipFPDelta?</li>
+ *   <li>Header, --&gt; {@link CodecUtil#writeIndexHeader IndexHeader}</li>
+ *   <li>PackedBlockSize, SingletonDocID --&gt; {@link DataOutput#writeVInt VInt}</li>
+ *   <li>DocFPDelta, PosFPDelta, PayFPDelta, PosVIntBlockFPDelta, SkipFPDelta --&gt; {@link DataOutput#writeVLong VLong}</li>
+ *   <li>Footer --&gt; {@link CodecUtil#writeFooter CodecFooter}</li>
+ * </ul>
+ * <p>Notes:
+ * <ul>
+ *    <li>Header is a {@link CodecUtil#writeIndexHeader IndexHeader} storing the version information
+ *        for the postings.</li>
+ *    <li>PackedBlockSize is the fixed block size for packed blocks. In packed block, bit width is 
+ *        determined by the largest integer. Smaller block size result in smaller variance among width 
+ *        of integers hence smaller indexes. Larger block size result in more efficient bulk i/o hence
+ *        better acceleration. This value should always be a multiple of 64, currently fixed as 128 as 
+ *        a tradeoff. It is also the skip interval used to accelerate {@link PostingsEnum#advance(int)}.
+ *    <li>DocFPDelta determines the position of this term's TermFreqs within the .doc file.
+ *        In particular, it is the difference of file offset between this term's
+ *        data and previous term's data (or zero, for the first term in the block).On disk it is
+ *        stored as the difference from previous value in sequence. </li>
+ *    <li>PosFPDelta determines the position of this term's TermPositions within the .pos file.
+ *        While PayFPDelta determines the position of this term's &lt;TermPayloads, TermOffsets?&gt; within
+ *        the .pay file. Similar to DocFPDelta, it is the difference between two file positions (or
+ *        neglected, for fields that omit payloads and offsets).</li>
+ *    <li>PosVIntBlockFPDelta determines the position of this term's last TermPosition in last pos packed
+ *        block within the .pos file. It is synonym for PayVIntBlockFPDelta or OffsetVIntBlockFPDelta.
+ *        This is actually used to indicate whether it is necessary to load following
+ *        payloads and offsets from .pos instead of .pay. Every time a new block of positions are to be
+ *        loaded, the PostingsReader will use this value to check whether current block is packed format
+ *        or VInt. When packed format, payloads and offsets are fetched from .pay, otherwise from .pos.
+ *        (this value is neglected when total number of positions i.e. totalTermFreq is less or equal
+ *        to PackedBlockSize).
+ *    <li>SkipFPDelta determines the position of this term's SkipData within the .doc
+ *        file. In particular, it is the length of the TermFreq data.
+ *        SkipDelta is only stored if DocFreq is not smaller than SkipMinimum
+ *        (i.e. 128 in Lucene50PostingsFormat).</li>
+ *    <li>SingletonDocID is an optimization when a term only appears in one document. In this case, instead
+ *        of writing a file pointer to the .doc file (DocFPDelta), and then a VIntBlock at that location, the
+ *        single document ID is written to the term dictionary.</li>
+ * </ul>
+ * </dd>
+ * </dl>
+ *
+ * <a name="Termindex"></a>
+ * <dl>
+ * <dd>
+ * <b>Term Index</b>
+ * <p>The .tip file contains an index into the term dictionary, so that it can be
+ * accessed randomly.  See {@link BlockTreeTermsWriter} for more details on the format.
+ * </dd>
+ * </dl>
+ *
+ *
+ * <a name="Frequencies"></a>
+ * <dl>
+ * <dd>
+ * <b>Frequencies and Skip Data</b>
+ *
+ * <p>The .doc file contains the lists of documents which contain each term, along
+ * with the frequency of the term in that document (except when frequencies are
+ * omitted: {@link IndexOptions#DOCS}). It also saves skip data to the beginning of
+ * each packed or VInt block, when the length of document list is larger than packed block size.</p>
+ *
+ * <ul>
+ *   <li>docFile(.doc) --&gt; Header, &lt;TermFreqs, SkipData?&gt;<sup>TermCount</sup>, Footer</li>
+ *   <li>Header --&gt; {@link CodecUtil#writeIndexHeader IndexHeader}</li>
+ *   <li>TermFreqs --&gt; &lt;PackedBlock&gt; <sup>PackedDocBlockNum</sup>,
+ *                        VIntBlock? </li>
+ *   <li>PackedBlock --&gt; PackedDocDeltaBlock, PackedFreqBlock?
+ *   <li>VIntBlock --&gt; &lt;DocDelta[, Freq?]&gt;<sup>DocFreq-PackedBlockSize*PackedDocBlockNum</sup>
+ *   <li>SkipData --&gt; &lt;&lt;SkipLevelLength, SkipLevel&gt;
+ *       <sup>NumSkipLevels-1</sup>, SkipLevel&gt;, SkipDatum?</li>
+ *   <li>SkipLevel --&gt; &lt;SkipDatum&gt; <sup>TrimmedDocFreq/(PackedBlockSize^(Level + 1))</sup></li>
+ *   <li>SkipDatum --&gt; DocSkip, DocFPSkip, &lt;PosFPSkip, PosBlockOffset, PayLength?,
+ *                        PayFPSkip?&gt;?, SkipChildLevelPointer?</li>
+ *   <li>PackedDocDeltaBlock, PackedFreqBlock --&gt; {@link PackedInts PackedInts}</li>
+ *   <li>DocDelta, Freq, DocSkip, DocFPSkip, PosFPSkip, PosBlockOffset, PayByteUpto, PayFPSkip
+ *       --&gt;
+ *   {@link DataOutput#writeVInt VInt}</li>
+ *   <li>SkipChildLevelPointer --&gt; {@link DataOutput#writeVLong VLong}</li>
+ *   <li>Footer --&gt; {@link CodecUtil#writeFooter CodecFooter}</li>
+ * </ul>
+ * <p>Notes:
+ * <ul>
+ *   <li>PackedDocDeltaBlock is theoretically generated from two steps:
+ *     <ol>
+ *       <li>Calculate the difference between each document number and previous one,
+ *           and get a d-gaps list (for the first document, use absolute value); </li>
+ *       <li>For those d-gaps from first one to PackedDocBlockNum*PackedBlockSize<sup>th</sup>,
+ *           separately encode as packed blocks.</li>
+ *     </ol>
+ *     If frequencies are not omitted, PackedFreqBlock will be generated without d-gap step.
+ *   </li>
+ *   <li>VIntBlock stores remaining d-gaps (along with frequencies when possible) with a format
+ *       that encodes DocDelta and Freq:
+ *       <p>DocDelta: if frequencies are indexed, this determines both the document
+ *       number and the frequency. In particular, DocDelta/2 is the difference between
+ *       this document number and the previous document number (or zero when this is the
+ *       first document in a TermFreqs). When DocDelta is odd, the frequency is one.
+ *       When DocDelta is even, the frequency is read as another VInt. If frequencies
+ *       are omitted, DocDelta contains the gap (not multiplied by 2) between document
+ *       numbers and no frequency information is stored.</p>
+ *       <p>For example, the TermFreqs for a term which occurs once in document seven
+ *          and three times in document eleven, with frequencies indexed, would be the
+ *          following sequence of VInts:</p>
+ *       <p>15, 8, 3</p>
+ *       <p>If frequencies were omitted ({@link IndexOptions#DOCS}) it would be this
+ *          sequence of VInts instead:</p>
+ *       <p>7,4</p>
+ *   </li>
+ *   <li>PackedDocBlockNum is the number of packed blocks for current term's docids or frequencies.
+ *       In particular, PackedDocBlockNum = floor(DocFreq/PackedBlockSize) </li>
+ *   <li>TrimmedDocFreq = DocFreq % PackedBlockSize == 0 ? DocFreq - 1 : DocFreq.
+ *       We use this trick since the definition of skip entry is a little different from base interface.
+ *       In {@link MultiLevelSkipListWriter}, skip data is assumed to be saved for
+ *       skipInterval<sup>th</sup>, 2*skipInterval<sup>th</sup> ... posting in the list. However,
+ *       in Lucene50PostingsFormat, the skip data is saved for skipInterval+1<sup>th</sup>,
+ *       2*skipInterval+1<sup>th</sup> ... posting (skipInterval==PackedBlockSize in this case).
+ *       When DocFreq is multiple of PackedBlockSize, MultiLevelSkipListWriter will expect one
+ *       more skip data than Lucene50SkipWriter. </li>
+ *   <li>SkipDatum is the metadata of one skip entry.
+ *      For the first block (no matter packed or VInt), it is omitted.</li>
+ *   <li>DocSkip records the document number of every PackedBlockSize<sup>th</sup> document number in
+ *       the postings (i.e. last document number in each packed block). On disk it is stored as the
+ *       difference from previous value in the sequence. </li>
+ *   <li>DocFPSkip records the file offsets of each block (excluding )posting at
+ *       PackedBlockSize+1<sup>th</sup>, 2*PackedBlockSize+1<sup>th</sup> ... , in DocFile.
+ *       The file offsets are relative to the start of current term's TermFreqs.
+ *       On disk it is also stored as the difference from previous SkipDatum in the sequence.</li>
+ *   <li>Since positions and payloads are also block encoded, the skip should skip to related block first,
+ *       then fetch the values according to in-block offset. PosFPSkip and PayFPSkip record the file
+ *       offsets of related block in .pos and .pay, respectively. While PosBlockOffset indicates
+ *       which value to fetch inside the related block (PayBlockOffset is unnecessary since it is always
+ *       equal to PosBlockOffset). Same as DocFPSkip, the file offsets are relative to the start of
+ *       current term's TermFreqs, and stored as a difference sequence.</li>
+ *   <li>PayByteUpto indicates the start offset of the current payload. It is equivalent to
+ *       the sum of the payload lengths in the current block up to PosBlockOffset</li>
+ * </ul>
+ * </dd>
+ * </dl>
+ *
+ * <a name="Positions"></a>
+ * <dl>
+ * <dd>
+ * <b>Positions</b>
+ * <p>The .pos file contains the lists of positions that each term occurs at within documents. It also
+ *    sometimes stores part of payloads and offsets for speedup.</p>
+ * <ul>
+ *   <li>PosFile(.pos) --&gt; Header, &lt;TermPositions&gt; <sup>TermCount</sup>, Footer</li>
+ *   <li>Header --&gt; {@link CodecUtil#writeIndexHeader IndexHeader}</li>
+ *   <li>TermPositions --&gt; &lt;PackedPosDeltaBlock&gt; <sup>PackedPosBlockNum</sup>,
+ *                            VIntBlock? </li>
+ *   <li>VIntBlock --&gt; &lt;PositionDelta[, PayloadLength?], PayloadData?,
+ *                        OffsetDelta?, OffsetLength?&gt;<sup>PosVIntCount</sup>
+ *   <li>PackedPosDeltaBlock --&gt; {@link PackedInts PackedInts}</li>
+ *   <li>PositionDelta, OffsetDelta, OffsetLength --&gt;
+ *       {@link DataOutput#writeVInt VInt}</li>
+ *   <li>PayloadData --&gt; {@link DataOutput#writeByte byte}<sup>PayLength</sup></li>
+ *   <li>Footer --&gt; {@link CodecUtil#writeFooter CodecFooter}</li>
+ * </ul>
+ * <p>Notes:
+ * <ul>
+ *   <li>TermPositions are order by term (terms are implicit, from the term dictionary), and position
+ *       values for each term document pair are incremental, and ordered by document number.</li>
+ *   <li>PackedPosBlockNum is the number of packed blocks for current term's positions, payloads or offsets.
+ *       In particular, PackedPosBlockNum = floor(totalTermFreq/PackedBlockSize) </li>
+ *   <li>PosVIntCount is the number of positions encoded as VInt format. In particular,
+ *       PosVIntCount = totalTermFreq - PackedPosBlockNum*PackedBlockSize</li>
+ *   <li>The procedure how PackedPosDeltaBlock is generated is the same as PackedDocDeltaBlock
+ *       in chapter <a href="#Frequencies">Frequencies and Skip Data</a>.</li>
+ *   <li>PositionDelta is, if payloads are disabled for the term's field, the
+ *       difference between the position of the current occurrence in the document and
+ *       the previous occurrence (or zero, if this is the first occurrence in this
+ *       document). If payloads are enabled for the term's field, then PositionDelta/2
+ *       is the difference between the current and the previous position. If payloads
+ *       are enabled and PositionDelta is odd, then PayloadLength is stored, indicating
+ *       the length of the payload at the current term position.</li>
+ *   <li>For example, the TermPositions for a term which occurs as the fourth term in
+ *       one document, and as the fifth and ninth term in a subsequent document, would
+ *       be the following sequence of VInts (payloads disabled):
+ *       <p>4, 5, 4</p></li>
+ *   <li>PayloadData is metadata associated with the current term position. If
+ *       PayloadLength is stored at the current position, then it indicates the length
+ *       of this payload. If PayloadLength is not stored, then this payload has the same
+ *       length as the payload at the previous position.</li>
+ *   <li>OffsetDelta/2 is the difference between this position's startOffset from the
+ *       previous occurrence (or zero, if this is the first occurrence in this document).
+ *       If OffsetDelta is odd, then the length (endOffset-startOffset) differs from the
+ *       previous occurrence and an OffsetLength follows. Offset data is only written for
+ *       {@link IndexOptions#DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS}.</li>
+ * </ul>
+ * </dd>
+ * </dl>
+ *
+ * <a name="Payloads"></a>
+ * <dl>
+ * <dd>
+ * <b>Payloads and Offsets</b>
+ * <p>The .pay file will store payloads and offsets associated with certain term-document positions.
+ *    Some payloads and offsets will be separated out into .pos file, for performance reasons.
+ * <ul>
+ *   <li>PayFile(.pay): --&gt; Header, &lt;TermPayloads, TermOffsets?&gt; <sup>TermCount</sup>, Footer</li>
+ *   <li>Header --&gt; {@link CodecUtil#writeIndexHeader IndexHeader}</li>
+ *   <li>TermPayloads --&gt; &lt;PackedPayLengthBlock, SumPayLength, PayData&gt; <sup>PackedPayBlockNum</sup>
+ *   <li>TermOffsets --&gt; &lt;PackedOffsetStartDeltaBlock, PackedOffsetLengthBlock&gt; <sup>PackedPayBlockNum</sup>
+ *   <li>PackedPayLengthBlock, PackedOffsetStartDeltaBlock, PackedOffsetLengthBlock --&gt; {@link PackedInts PackedInts}</li>
+ *   <li>SumPayLength --&gt; {@link DataOutput#writeVInt VInt}</li>
+ *   <li>PayData --&gt; {@link DataOutput#writeByte byte}<sup>SumPayLength</sup></li>
+ *   <li>Footer --&gt; {@link CodecUtil#writeFooter CodecFooter}</li>
+ * </ul>
+ * <p>Notes:
+ * <ul>
+ *   <li>The order of TermPayloads/TermOffsets will be the same as TermPositions, note that part of
+ *       payload/offsets are stored in .pos.</li>
+ *   <li>The procedure how PackedPayLengthBlock and PackedOffsetLengthBlock are generated is the
+ *       same as PackedFreqBlock in chapter <a href="#Frequencies">Frequencies and Skip Data</a>.
+ *       While PackedStartDeltaBlock follows a same procedure as PackedDocDeltaBlock.</li>
+ *   <li>PackedPayBlockNum is always equal to PackedPosBlockNum, for the same term. It is also synonym
+ *       for PackedOffsetBlockNum.</li>
+ *   <li>SumPayLength is the total length of payloads written within one block, should be the sum
+ *       of PayLengths in one packed block.</li>
+ *   <li>PayLength in PackedPayLengthBlock is the length of each payload associated with the current
+ *       position.</li>
+ * </ul>
+ * </dd>
+ * </dl>
+ *
+ * @lucene.experimental
+ */
+
+public abstract class EncryptedLucene50PostingsFormat extends PostingsFormat {
+
+  private final int minTermBlockSize;
+  private final int maxTermBlockSize;
+
+  /**
+   * Fixed packed block size, number of integers encoded in
+   * a single packed block.
+   */
+  // NOTE: must be multiple of 64 because of PackedInts long-aligned encoding/decoding
+  public final static int BLOCK_SIZE = 128;
+
+  private static final String CODEC_NAME = "EncryptedLucene50";
+
+  /** Creates {@code EncryptedLucene50PostingsFormat} with default
+   *  settings. */
+  public EncryptedLucene50PostingsFormat() {
+    this(EncryptedBlockTreeTermsWriter.DEFAULT_MIN_BLOCK_SIZE, EncryptedBlockTreeTermsWriter.DEFAULT_MAX_BLOCK_SIZE);
+  }
+
+  /** Creates {@code EncryptedLucene50PostingsFormat} with custom
+   *  values for {@code minBlockSize} and {@code
+   *  maxBlockSize} passed to block terms dictionary.
+   *  @see BlockTreeTermsWriter#BlockTreeTermsWriter(SegmentWriteState,PostingsWriterBase,int,int) */
+  public EncryptedLucene50PostingsFormat(int minTermBlockSize, int maxTermBlockSize) {
+    super(CODEC_NAME);
+    EncryptedBlockTreeTermsWriter.validateSettings(minTermBlockSize, maxTermBlockSize);
+    this.minTermBlockSize = minTermBlockSize;
+    this.maxTermBlockSize = maxTermBlockSize;
+  }
+
+  /**
+   * Concrete implementation should specify the {@link CipherFactory}.
+   */
+  protected abstract CipherFactory getCipherFactory();
+
+  @Override
+  public String toString() {
+    return getName() + "(blocksize=" + BLOCK_SIZE + ")";
+  }
+
+  @Override
+  public FieldsConsumer fieldsConsumer(SegmentWriteState state) throws IOException {
+    // Store cipher version in segment info
+    SegmentInfo si = state.segmentInfo;
+    long cipherVersion = this.getCipherFactory().getEncipherVersion();
+    // It might overwrite the value put by another format, e.g., EncryptedLucene50StoredFieldsFormat
+    // We just check that it is the same version.
+    String previous = si.putAttribute(CipherFactory.CIPHER_VERSION_KEY, Long.toString(cipherVersion));
+    if (previous != null && Long.parseLong(previous) != cipherVersion) {
+      throw new IllegalStateException("found existing value for " + CipherFactory.CIPHER_VERSION_KEY +
+          " for segment: " + si.name + "old=" + previous + ", new=" + cipherVersion);
+    }
+
+    PostingsWriterBase postingsWriter = new Lucene50PostingsWriter(state);
+
+    boolean success = false;
+    try {
+      FieldsConsumer ret = new EncryptedBlockTreeTermsWriter(this.getCipherFactory(),
+                                                    state,
+                                                    postingsWriter,
+                                                    minTermBlockSize, 
+                                                    maxTermBlockSize);
+      success = true;
+      return ret;
+    } finally {
+      if (!success) {
+        IOUtils.closeWhileHandlingException(postingsWriter);
+      }
+    }
+  }
+
+  @Override
+  public FieldsProducer fieldsProducer(SegmentReadState state) throws IOException {
+    SegmentInfo si = state.segmentInfo;
+    String value = si.getAttribute(CipherFactory.CIPHER_VERSION_KEY);
+    if (value == null) {
+      throw new IllegalStateException("missing value for " + CipherFactory.CIPHER_VERSION_KEY +
+          " for segment: " + si.name);
+    }
+
+    PostingsReaderBase postingsReader = new Lucene50PostingsReader(state);
+    boolean success = false;
+    try {
+      FieldsProducer ret = new EncryptedBlockTreeTermsReader(this.getCipherFactory(), postingsReader, state);
+      success = true;
+      return ret;
+    } finally {
+      if (!success) {
+        IOUtils.closeWhileHandlingException(postingsReader);
+      }
+    }
+  }
+
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/EncryptedLucene50StoredFieldsFormat.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/EncryptedLucene50StoredFieldsFormat.java
new file mode 100644
index 0000000..1c433ea
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/EncryptedLucene50StoredFieldsFormat.java
@@ -0,0 +1,229 @@
+package org.apache.lucene.codecs.encrypted;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.CodecUtil;
+import org.apache.lucene.codecs.StoredFieldsFormat;
+import org.apache.lucene.codecs.StoredFieldsReader;
+import org.apache.lucene.codecs.StoredFieldsWriter;
+import org.apache.lucene.codecs.compressing.CompressionMode;
+import org.apache.lucene.codecs.encrypted.compressing.EncryptedCompressingStoredFieldsFormat;
+import org.apache.lucene.index.FieldInfos;
+import org.apache.lucene.index.SegmentInfo;
+import org.apache.lucene.index.StoredFieldVisitor;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.IOContext;
+import org.apache.lucene.util.packed.PackedInts;
+
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * Lucene 5.0 encrypted stored fields format. This is an extension of
+ * {@link org.apache.lucene.codecs.compressing.CompressingStoredFieldsFormat} that enables the encryption of
+ * data on a field-level. Document fields that are encrypted are compressed in a second compressed block.
+ *
+ * <p><b>Principle</b>
+ * <p>This {@link StoredFieldsFormat} compresses blocks of documents in order to improve the compression ratio
+ * compared to document-level compression. It uses the <a href="http://code.google.com/p/lz4/">LZ4</a>
+ * compression algorithm by default in 16KB blocks, which is fast to compress 
+ * and very fast to decompress data. Although the default compression method 
+ * that is used ({@link Mode#BEST_SPEED BEST_SPEED}) focuses more on speed than on 
+ * compression ratio, it should provide interesting compression ratios
+ * for redundant inputs (such as log files, HTML or plain text). For higher
+ * compression, you can choose ({@link Mode#BEST_COMPRESSION BEST_COMPRESSION}), which uses 
+ * the <a href="http://en.wikipedia.org/wiki/DEFLATE">DEFLATE</a> algorithm with 60KB blocks 
+ * for a better ratio at the expense of slower performance. 
+ * These two options can be configured like this:
+ * <pre class="prettyprint">
+ *   // the default: for high performance
+ *   indexWriterConfig.setCodec(new Lucene53Codec(Mode.BEST_SPEED));
+ *   // instead for higher performance (but slower):
+ *   // indexWriterConfig.setCodec(new Lucene53Codec(Mode.BEST_COMPRESSION));
+ * </pre>
+ * <p><b>File formats</b>
+ * <p>Stored fields are represented by two files:
+ * <ol>
+ * <li><a name="field_data"></a>
+ * <p>A fields data file (extension <tt>.fdt</tt>). This file stores a compact
+ * representation of documents in compressed blocks of 16KB or more. When
+ * writing a segment, documents are appended to an in-memory <tt>byte[]</tt>
+ * buffer. When its size reaches 16KB or more, some metadata about the documents
+ * is flushed to disk, immediately followed by a compressed representation of
+ * the buffer using the
+ * <a href="http://code.google.com/p/lz4/">LZ4</a>
+ * <a href="http://fastcompression.blogspot.fr/2011/05/lz4-explained.html">compression format</a>.</p>
+ * <p>Here is a more detailed description of the field data file format:</p>
+ * <ul>
+ * <li>FieldData (.fdt) --&gt; &lt;Header&gt;, PackedIntsVersion, &lt;Chunk&gt;<sup>ChunkCount</sup>, ChunkCount, DirtyChunkCount, Footer</li>
+ * <li>Header --&gt; {@link CodecUtil#writeIndexHeader IndexHeader}</li>
+ * <li>PackedIntsVersion --&gt; {@link PackedInts#VERSION_CURRENT} as a {@link DataOutput#writeVInt VInt}</li>
+ * <li>ChunkCount is not known in advance and is the number of chunks necessary to store all document of the segment</li>
+ * <li>Chunk --&gt; DocBase, ChunkDocs, DocFieldCounts, DocLengths, &lt;CompressedDocs&gt;, EncryptedDocFieldCounts, EncryptedDocLengths, &lt;EncryptedCompressedDocs&gt;</li>
+ * <li>DocBase --&gt; the ID of the first document of the chunk as a {@link DataOutput#writeVInt VInt}</li>
+ * <li>ChunkDocs --&gt; the number of documents in the chunk as a {@link DataOutput#writeVInt VInt}</li>
+ * <li>DocFieldCounts, EncryptedDocFieldCounts --&gt; the number of stored fields of every document in the chunk, encoded as followed:<ul>
+ *   <li>if chunkDocs=1, the unique value is encoded as a {@link DataOutput#writeVInt VInt}</li>
+ *   <li>else read a {@link DataOutput#writeVInt VInt} (let's call it <tt>bitsRequired</tt>)<ul>
+ *     <li>if <tt>bitsRequired</tt> is <tt>0</tt> then all values are equal, and the common value is the following {@link DataOutput#writeVInt VInt}</li>
+ *     <li>else <tt>bitsRequired</tt> is the number of bits required to store any value, and values are stored in a {@link PackedInts packed} array where every value is stored on exactly <tt>bitsRequired</tt> bits</li>
+ *   </ul></li>
+ * </ul></li>
+ * <li>DocLengths, EncryptedDocLengths --&gt; the lengths of all documents in the chunk, encoded with the same method as DocFieldCounts</li>
+ * <li>CompressedDocsLength --&gt; the length in bytes of CompressedDocs</li>
+ * <li>CompressedDocs, EncryptedCompressedDocs --&gt; a compressed representation of &lt;Docs&gt; using the LZ4 compression format</li>
+ * <li>Docs --&gt; &lt;Doc&gt;<sup>ChunkDocs</sup></li>
+ * <li>Doc --&gt; &lt;FieldNumAndType, Value&gt;<sup>DocFieldCount</sup></li>
+ * <li>FieldNumAndType --&gt; a {@link DataOutput#writeVLong VLong}, whose 3 last bits are Type and other bits are FieldNum</li>
+ * <li>Type --&gt;<ul>
+ *   <li>0: Value is String</li>
+ *   <li>1: Value is BinaryValue</li>
+ *   <li>2: Value is Int</li>
+ *   <li>3: Value is Float</li>
+ *   <li>4: Value is Long</li>
+ *   <li>5: Value is Double</li>
+ *   <li>6, 7: unused</li>
+ * </ul></li>
+ * <li>FieldNum --&gt; an ID of the field</li>
+ * <li>Value --&gt; {@link DataOutput#writeString(String) String} | BinaryValue | Int | Float | Long | Double depending on Type</li>
+ * <li>BinaryValue --&gt; ValueLength &lt;Byte&gt;<sup>ValueLength</sup></li>
+ * <li>ChunkCount --&gt; the number of chunks in this file</li>
+ * <li>DirtyChunkCount --&gt; the number of prematurely flushed chunks in this file</li>
+ * <li>Footer --&gt; {@link CodecUtil#writeFooter CodecFooter}</li>
+ * </ul>
+ * <p>Notes
+ * <ul>
+ * <li>If documents are larger than 16KB then chunks will likely contain only
+ * one document. However, documents can never spread across several chunks (all
+ * fields of a single document are in the same chunk).</li>
+ * <li>When at least one document in a chunk is large enough so that the chunk
+ * is larger than 32KB, the chunk will actually be compressed in several LZ4
+ * blocks of 16KB. This allows {@link StoredFieldVisitor}s which are only
+ * interested in the first fields of a document to not have to decompress 10MB
+ * of data if the document is 10MB, but only 16KB.</li>
+ * <li>Given that the original lengths are written in the metadata of the chunk,
+ * the decompressor can leverage this information to stop decoding as soon as
+ * enough data has been decompressed.</li>
+ * <li>In case documents are incompressible, CompressedDocs will be less than
+ * 0.5% larger than Docs.</li>
+ * </ul>
+ * </li>
+ * <li><a name="field_index"></a>
+ * <p>A fields index file (extension <tt>.fdx</tt>).</p>
+ * <ul>
+ * <li>FieldsIndex (.fdx) --&gt; &lt;Header&gt;, &lt;ChunkIndex&gt;, Footer</li>
+ * <li>Header --&gt; {@link CodecUtil#writeIndexHeader IndexHeader}</li>
+ * <li>ChunkIndex: See {@link org.apache.lucene.codecs.encrypted.compressing.CompressingStoredFieldsIndexWriter}</li>
+ * <li>Footer --&gt; {@link CodecUtil#writeFooter CodecFooter}</li>
+ * </ul>
+ * </li>
+ * </ol>
+ * <p><b>Known limitations</b>
+ * <p>This {@link StoredFieldsFormat} does not support individual documents
+ * larger than (<tt>2<sup>31</sup> - 2<sup>14</sup></tt>) bytes.
+ * <p>This {@link StoredFieldsFormat} does not maintain the order of stored fields.</p>
+ * @lucene.experimental
+ */
+public abstract class EncryptedLucene50StoredFieldsFormat extends StoredFieldsFormat {
+
+  /** Configuration option for stored fields. */
+  public static enum Mode {
+    /** Trade compression ratio for retrieval speed. */
+    BEST_SPEED,
+    /** Trade retrieval speed for compression ratio. */
+    BEST_COMPRESSION
+  }
+
+  /** Attribute key for compression mode. */
+  public static final String MODE_KEY = EncryptedLucene50StoredFieldsFormat.class.getSimpleName() + ".mode";
+
+  final Mode mode;
+
+  /** Stored fields format with default options */
+  public EncryptedLucene50StoredFieldsFormat() {
+    this(Mode.BEST_SPEED);
+  }
+
+  /** Stored fields format with specified mode */
+  public EncryptedLucene50StoredFieldsFormat(Mode mode) {
+    this.mode = Objects.requireNonNull(mode);
+  }
+
+  /**
+   * Concrete implementation should specify the {@link CipherFactory}.
+   */
+  protected abstract CipherFactory getCipherFactory();
+
+  @Override
+  public StoredFieldsReader fieldsReader(Directory directory, SegmentInfo si, FieldInfos fn, IOContext context) throws IOException {
+    String value = si.getAttribute(CipherFactory.CIPHER_VERSION_KEY);
+    if (value == null) {
+      throw new IllegalStateException("missing value for " + CipherFactory.CIPHER_VERSION_KEY +
+          " for segment: " + si.name);
+    }
+
+    value = si.getAttribute(MODE_KEY);
+    if (value == null) {
+      throw new IllegalStateException("missing value for " + MODE_KEY + " for segment: " + si.name);
+    }
+    Mode mode = Mode.valueOf(value);
+
+    return impl(mode).fieldsReader(directory, si, fn, context);
+  }
+
+  @Override
+  public StoredFieldsWriter fieldsWriter(Directory directory, SegmentInfo si, IOContext context) throws IOException {
+    // Store cipher version in segment info
+    long cipherVersion = this.getCipherFactory().getEncipherVersion();
+    // It might overwrite the value put by another format, e.g., EncryptedLucene50PostingsFormat
+    // We just check that it is the same version.
+    String previous = si.putAttribute(CipherFactory.CIPHER_VERSION_KEY, Long.toString(cipherVersion));
+    if (previous != null && Long.parseLong(previous) != cipherVersion) {
+      throw new IllegalStateException("found existing value for " + CipherFactory.CIPHER_VERSION_KEY +
+          " for segment: " + si.name + "old=" + previous + ", new=" + cipherVersion);
+    }
+
+    previous = si.putAttribute(MODE_KEY, mode.name());
+    if (previous != null) {
+      throw new IllegalStateException("found existing value for " + MODE_KEY + " for segment: " + si.name +
+                                      "old=" + previous + ", new=" + mode.name());
+    }
+
+    return impl(mode).fieldsWriter(directory, si, context);
+  }
+  
+  StoredFieldsFormat impl(Mode mode) {
+    CipherFactory cipherFactory = this.getCipherFactory();
+    switch (mode) {
+      case BEST_SPEED: 
+        return newEncryptedCompressingStoredFieldsFormat("EncryptedLucene50StoredFieldsFast", cipherFactory, CompressionMode.FAST, 1 << 14, 128, 1024);
+      case BEST_COMPRESSION: 
+        return newEncryptedCompressingStoredFieldsFormat("EncryptedLucene50StoredFieldsHigh", cipherFactory, CompressionMode.HIGH_COMPRESSION, 61440, 512, 1024);
+      default: throw new AssertionError();
+    }
+  }
+
+  /**
+   * Can be override by sub-classes to change the implementation of the {@link EncryptedCompressingStoredFieldsFormat}.
+   */
+  protected abstract StoredFieldsFormat newEncryptedCompressingStoredFieldsFormat(String formatName, CipherFactory cipherFactory,
+                                                                                  CompressionMode compressionMode,
+                                                                                  int chunkSize, int maxDocsPerChunk, int blockSize);
+
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/EncryptedLucene50TermVectorsFormat.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/EncryptedLucene50TermVectorsFormat.java
new file mode 100644
index 0000000..e8470c3
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/EncryptedLucene50TermVectorsFormat.java
@@ -0,0 +1,134 @@
+package org.apache.lucene.codecs.encrypted;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.CodecUtil;
+import org.apache.lucene.codecs.TermVectorsFormat;
+import org.apache.lucene.codecs.compressing.CompressingStoredFieldsIndexWriter;
+import org.apache.lucene.codecs.compressing.CompressionMode;
+import org.apache.lucene.codecs.encrypted.compressing.EncryptedCompressingTermVectorsFormat;
+import org.apache.lucene.codecs.lucene50.Lucene50StoredFieldsFormat;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.util.packed.BlockPackedWriter;
+import org.apache.lucene.util.packed.PackedInts;
+
+/**
+ * Lucene 5.0 {@link TermVectorsFormat term vectors format}.
+ * <p>
+ * Very similarly to {@link Lucene50StoredFieldsFormat}, this format is based
+ * on compressed chunks of data, with document-level granularity so that a
+ * document can never span across distinct chunks. Moreover, data is made as
+ * compact as possible:<ul>
+ * <li>textual data is compressed using the very light,
+ * <a href="http://code.google.com/p/lz4/">LZ4</a> compression algorithm,
+ * <li>binary data is written using fixed-size blocks of
+ * {@link PackedInts packed ints}.
+ * </ul>
+ * <p>
+ * Term vectors are stored using two files<ul>
+ * <li>a data file where terms, frequencies, positions, offsets and payloads
+ * are stored,
+ * <li>an index file, loaded into memory, used to locate specific documents in
+ * the data file.
+ * </ul>
+ * Looking up term vectors for any document requires at most 1 disk seek.
+ * <p><b>File formats</b>
+ * <ol>
+ * <li><a name="vector_data"></a>
+ * <p>A vector data file (extension <tt>.tvd</tt>). This file stores terms,
+ * frequencies, positions, offsets and payloads for every document. Upon writing
+ * a new segment, it accumulates data into memory until the buffer used to store
+ * terms and payloads grows beyond 4KB. Then it flushes all metadata, terms
+ * and positions to disk using <a href="http://code.google.com/p/lz4/">LZ4</a>
+ * compression for terms and payloads and
+ * {@link BlockPackedWriter blocks of packed ints} for positions.</p>
+ * <p>Here is a more detailed description of the field data file format:</p>
+ * <ul>
+ * <li>VectorData (.tvd) --&gt; &lt;Header&gt;, PackedIntsVersion, ChunkSize, &lt;Chunk&gt;<sup>ChunkCount</sup>, ChunkCount, DirtyChunkCount, Footer</li>
+ * <li>Header --&gt; {@link CodecUtil#writeIndexHeader IndexHeader}</li>
+ * <li>PackedIntsVersion --&gt; {@link PackedInts#VERSION_CURRENT} as a {@link DataOutput#writeVInt VInt}</li>
+ * <li>ChunkSize is the number of bytes of terms to accumulate before flushing, as a {@link DataOutput#writeVInt VInt}</li>
+ * <li>ChunkCount is not known in advance and is the number of chunks necessary to store all document of the segment</li>
+ * <li>Chunk --&gt; DocBase, ChunkDocs, &lt; NumFields &gt;, &lt; FieldNums &gt;, &lt; FieldNumOffs &gt;, &lt; Flags &gt;,
+ * &lt; NumTerms &gt;, &lt; TermLengths &gt;, &lt; TermFreqs &gt;, &lt; Positions &gt;, &lt; StartOffsets &gt;, &lt; Lengths &gt;,
+ * &lt; PayloadLengths &gt;, &lt; TermAndPayloads &gt;</li>
+ * <li>DocBase is the ID of the first doc of the chunk as a {@link DataOutput#writeVInt VInt}</li>
+ * <li>ChunkDocs is the number of documents in the chunk</li>
+ * <li>NumFields --&gt; DocNumFields<sup>ChunkDocs</sup></li>
+ * <li>DocNumFields is the number of fields for each doc, written as a {@link DataOutput#writeVInt VInt} if ChunkDocs==1 and as a {@link PackedInts} array otherwise</li>
+ * <li>FieldNums --&gt; FieldNumDelta<sup>TotalDistincFields</sup>, a delta-encoded list of the sorted unique field numbers present in the chunk</li>
+ * <li>FieldNumOffs --&gt; FieldNumOff<sup>TotalFields</sup>, as a {@link PackedInts} array</li>
+ * <li>FieldNumOff is the offset of the field number in FieldNums</li>
+ * <li>TotalFields is the total number of fields (sum of the values of NumFields)</li>
+ * <li>Flags --&gt; Bit &lt; FieldFlags &gt;</li>
+ * <li>Bit  is a single bit which when true means that fields have the same options for every document in the chunk</li>
+ * <li>FieldFlags --&gt; if Bit==1: Flag<sup>TotalDistinctFields</sup> else Flag<sup>TotalFields</sup></li>
+ * <li>Flag: a 3-bits int where:<ul>
+ * <li>the first bit means that the field has positions</li>
+ * <li>the second bit means that the field has offsets</li>
+ * <li>the third bit means that the field has payloads</li>
+ * </ul></li>
+ * <li>NumTerms --&gt; FieldNumTerms<sup>TotalFields</sup></li>
+ * <li>FieldNumTerms: the number of terms for each field, using {@link BlockPackedWriter blocks of 64 packed ints}</li>
+ * <li>TermLengths --&gt; PrefixLength<sup>TotalTerms</sup> SuffixLength<sup>TotalTerms</sup></li>
+ * <li>TotalTerms: total number of terms (sum of NumTerms)</li>
+ * <li>PrefixLength: 0 for the first term of a field, the common prefix with the previous term otherwise using {@link BlockPackedWriter blocks of 64 packed ints}</li>
+ * <li>SuffixLength: length of the term minus PrefixLength for every term using {@link BlockPackedWriter blocks of 64 packed ints}</li>
+ * <li>TermFreqs --&gt; TermFreqMinus1<sup>TotalTerms</sup></li>
+ * <li>TermFreqMinus1: (frequency - 1) for each term using  {@link BlockPackedWriter blocks of 64 packed ints}</li>
+ * <li>Positions --&gt; PositionDelta<sup>TotalPositions</sup></li>
+ * <li>TotalPositions is the sum of frequencies of terms of all fields that have positions</li>
+ * <li>PositionDelta: the absolute position for the first position of a term, and the difference with the previous positions for following positions using {@link BlockPackedWriter blocks of 64 packed ints}</li>
+ * <li>StartOffsets --&gt; (AvgCharsPerTerm<sup>TotalDistinctFields</sup>) StartOffsetDelta<sup>TotalOffsets</sup></li>
+ * <li>TotalOffsets is the sum of frequencies of terms of all fields that have offsets</li>
+ * <li>AvgCharsPerTerm: average number of chars per term, encoded as a float on 4 bytes. They are not present if no field has both positions and offsets enabled.</li>
+ * <li>StartOffsetDelta: (startOffset - previousStartOffset - AvgCharsPerTerm * PositionDelta). previousStartOffset is 0 for the first offset and AvgCharsPerTerm is 0 if the field has no positions using  {@link BlockPackedWriter blocks of 64 packed ints}</li>
+ * <li>Lengths --&gt; LengthMinusTermLength<sup>TotalOffsets</sup></li>
+ * <li>LengthMinusTermLength: (endOffset - startOffset - termLength) using  {@link BlockPackedWriter blocks of 64 packed ints}</li>
+ * <li>PayloadLengths --&gt; PayloadLength<sup>TotalPayloads</sup></li>
+ * <li>TotalPayloads is the sum of frequencies of terms of all fields that have payloads</li>
+ * <li>PayloadLength is the payload length encoded using  {@link BlockPackedWriter blocks of 64 packed ints}</li>
+ * <li>TermAndPayloads --&gt; LZ4-compressed representation of &lt; FieldTermsAndPayLoads &gt;<sup>TotalFields</sup></li>
+ * <li>FieldTermsAndPayLoads --&gt; Terms (Payloads)</li>
+ * <li>Terms: term bytes</li>
+ * <li>Payloads: payload bytes (if the field has payloads)</li>
+ * <li>ChunkCount --&gt; the number of chunks in this file</li>
+ * <li>DirtyChunkCount --&gt; the number of prematurely flushed chunks in this file</li>
+ * <li>Footer --&gt; {@link CodecUtil#writeFooter CodecFooter}</li>
+ * </ul>
+ * </li>
+ * <li><a name="vector_index"></a>
+ * <p>An index file (extension <tt>.tvx</tt>).
+ * <ul>
+ * <li>VectorIndex (.tvx) --&gt; &lt;Header&gt;, &lt;ChunkIndex&gt;, Footer</li>
+ * <li>Header --&gt; {@link CodecUtil#writeIndexHeader IndexHeader}</li>
+ * <li>ChunkIndex: See {@link CompressingStoredFieldsIndexWriter}</li>
+ * <li>Footer --&gt; {@link CodecUtil#writeFooter CodecFooter}</li>
+ * </ul>
+ * </li>
+ * </ol>
+ * @lucene.experimental
+ */
+public abstract class EncryptedLucene50TermVectorsFormat extends EncryptedCompressingTermVectorsFormat {
+
+  /** Sole constructor. */
+  public EncryptedLucene50TermVectorsFormat() {
+    super("EncryptedLucene50TermVectors", "", CompressionMode.FAST, 1 << 12, 1024);
+  }
+
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/EncryptedLucene60Codec.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/EncryptedLucene60Codec.java
new file mode 100644
index 0000000..7b09e3a
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/EncryptedLucene60Codec.java
@@ -0,0 +1,140 @@
+package org.apache.lucene.codecs.encrypted;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.Codec;
+import org.apache.lucene.codecs.CompoundFormat;
+import org.apache.lucene.codecs.DocValuesFormat;
+import org.apache.lucene.codecs.FieldInfosFormat;
+import org.apache.lucene.codecs.LiveDocsFormat;
+import org.apache.lucene.codecs.NormsFormat;
+import org.apache.lucene.codecs.PointsFormat;
+import org.apache.lucene.codecs.PostingsFormat;
+import org.apache.lucene.codecs.SegmentInfoFormat;
+import org.apache.lucene.codecs.TermVectorsFormat;
+import org.apache.lucene.codecs.lucene50.Lucene50CompoundFormat;
+import org.apache.lucene.codecs.lucene50.Lucene50LiveDocsFormat;
+import org.apache.lucene.codecs.lucene50.Lucene50SegmentInfoFormat;
+import org.apache.lucene.codecs.lucene50.Lucene50TermVectorsFormat;
+import org.apache.lucene.codecs.lucene53.Lucene53NormsFormat;
+import org.apache.lucene.codecs.lucene60.Lucene60FieldInfosFormat;
+import org.apache.lucene.codecs.lucene60.Lucene60PointsFormat;
+import org.apache.lucene.codecs.perfield.PerFieldDocValuesFormat;
+import org.apache.lucene.codecs.perfield.PerFieldPostingsFormat;
+
+public abstract class EncryptedLucene60Codec extends Codec {
+
+  private final TermVectorsFormat vectorsFormat = new Lucene50TermVectorsFormat();
+  private final FieldInfosFormat fieldInfosFormat = new Lucene60FieldInfosFormat();
+  private final SegmentInfoFormat segmentInfosFormat = new Lucene50SegmentInfoFormat();
+  private final LiveDocsFormat liveDocsFormat = new Lucene50LiveDocsFormat();
+  private final CompoundFormat compoundFormat = new Lucene50CompoundFormat();
+
+  private final PostingsFormat postingsFormat = new PerFieldPostingsFormat() {
+    @Override
+    public PostingsFormat getPostingsFormatForField(String field) {
+      return EncryptedLucene60Codec.this.getPostingsFormatForField(field);
+    }
+  };
+
+  private final DocValuesFormat docValuesFormat = new PerFieldDocValuesFormat() {
+    @Override
+    public DocValuesFormat getDocValuesFormatForField(String field) {
+      return EncryptedLucene60Codec.this.getDocValuesFormatForField(field);
+    }
+  };
+
+  /**
+   * Instantiates a new codec.
+   */
+  public EncryptedLucene60Codec() {
+    super("EncryptedLucene60");
+  }
+
+  /**
+   * Concrete implementation should specify the {@link CipherFactory}.
+   */
+  public abstract CipherFactory getCipherFactory();
+
+  /**
+   * Concrete implementation should specify the {@link EncryptedLucene50StoredFieldsFormat}.
+   */
+  public abstract EncryptedLucene50StoredFieldsFormat storedFieldsFormat();
+
+  @Override
+  public TermVectorsFormat termVectorsFormat() {
+    return vectorsFormat;
+  }
+
+  @Override
+  public final PostingsFormat postingsFormat() {
+    return postingsFormat;
+  }
+
+  @Override
+  public final FieldInfosFormat fieldInfosFormat() {
+    return fieldInfosFormat;
+  }
+
+  @Override
+  public final SegmentInfoFormat segmentInfoFormat() {
+    return segmentInfosFormat;
+  }
+
+  @Override
+  public final LiveDocsFormat liveDocsFormat() {
+    return liveDocsFormat;
+  }
+
+  @Override
+  public final CompoundFormat compoundFormat() {
+    return compoundFormat;
+  }
+
+  @Override
+  public final PointsFormat pointsFormat() {
+    return new Lucene60PointsFormat();
+  }
+
+  /** Concrete implementation should specify the postings format that should be used for writing
+   *  new segments of <code>field</code>.
+   */
+  public abstract PostingsFormat getPostingsFormatForField(String field);
+
+  /** Returns the docvalues format that should be used for writing
+   *  new segments of <code>field</code>.
+   */
+  public DocValuesFormat getDocValuesFormatForField(String field) {
+    return defaultDVFormat;
+  }
+
+  @Override
+  public final DocValuesFormat docValuesFormat() {
+    return docValuesFormat;
+  }
+
+  private final DocValuesFormat defaultDVFormat = DocValuesFormat.forName("Lucene54");
+
+  private final NormsFormat normsFormat = new Lucene53NormsFormat();
+
+  @Override
+  public final NormsFormat normsFormat() {
+    return normsFormat;
+  }
+
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/IndexUpgrader.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/IndexUpgrader.java
new file mode 100644
index 0000000..bdf2950
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/IndexUpgrader.java
@@ -0,0 +1,242 @@
+package org.apache.lucene.codecs.encrypted;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexCommit;
+import org.apache.lucene.index.IndexNotFoundException;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.KeepOnlyLastCommitDeletionPolicy;
+import org.apache.lucene.index.MergePolicy;
+import org.apache.lucene.index.SegmentCommitInfo;
+import org.apache.lucene.index.UpgradeIndexMergePolicy;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.apache.lucene.util.CommandLineUtil;
+import org.apache.lucene.util.InfoStream;
+import org.apache.lucene.util.PrintStreamInfoStream;
+import org.apache.lucene.util.SuppressForbidden;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collection;
+
+/**
+ * A fork of the {@link org.apache.lucene.index.IndexUpgrader} tool that upgrades
+ * all segments of an index that are encrypted with an old cipher version by re-encrypting
+ * them with the latest cipher version.to the current one.
+ * <p>
+ * This tool can be used from command line:
+ * <pre>
+ *   java -cp "lucene-core.jar:lucene-codecs.jar" org.apache.lucene.codecs.encrypted.IndexUpgrader [-delete-prior-commits] [-verbose] indexDir
+ * </pre>
+ * Alternatively this class can be instantiated and {@link #upgrade} invoked. It uses
+ * {@link CipherUpgradeIndexMergePolicy} and triggers the upgrade via an forceMerge request
+ * to {@link IndexWriter}.
+ * <p>
+ * This tool keeps only the last commit in an index; for this
+ * reason, if the incoming index has more than one commit, the tool
+ * refuses to run by default. Specify {@code -delete-prior-commits}
+ * to override this, allowing the tool to delete all but the last commit.
+ * From Java code this can be enabled by passing {@code true} to
+ * {@link #IndexUpgrader(Directory,InfoStream,boolean)}.
+ * <p>
+ * <b>Warning:</b> This tool may reorder documents if the index was partially
+ * upgraded before execution (e.g., documents were added). If your application relies
+ * on &quot;monotonicity&quot; of doc IDs (which means that the order in which the documents
+ * were added to the index is preserved), do a full forceMerge instead.
+ * The {@link MergePolicy} set by {@link IndexWriterConfig} may also reorder
+ * documents.
+ *
+ * @see org.apache.lucene.index.IndexUpgrader
+ */
+public final class IndexUpgrader {
+  
+  private static final String LOG_PREFIX = "IndexUpgrader";
+
+  @SuppressForbidden(reason = "System.out required: command line tool")
+  private static void printUsage() {
+    System.err.println("Upgrades an index so all segments encrypted with a previous cipher version are rewritten.");
+    System.err.println("Usage:");
+    System.err.println("  java " + IndexUpgrader.class.getName() + " [-delete-prior-commits] [-verbose] [-dir-impl X] indexDir");
+    System.err.println("This tool keeps only the last commit in an index; for this");
+    System.err.println("reason, if the incoming index has more than one commit, the tool");
+    System.err.println("refuses to run by default. Specify -delete-prior-commits to override");
+    System.err.println("this, allowing the tool to delete all but the last commit.");
+    System.err.println("Specify a " + FSDirectory.class.getSimpleName() + 
+        " implementation through the -dir-impl option to force its use. If no package is specified the " 
+        + FSDirectory.class.getPackage().getName() + " package will be used.");
+    System.err.println("WARNING: This tool may reorder document IDs!");
+    System.exit(1);
+  }
+
+  /** Main method to run {code IndexUpgrader} from the
+   *  command-line. */
+  @SuppressWarnings("deprecation")
+  public static void main(String[] args) throws IOException {
+    parseArgs(args).upgrade();
+  }
+  
+  @SuppressForbidden(reason = "System.out required: command line tool")
+  static IndexUpgrader parseArgs(String[] args) throws IOException {
+    String path = null;
+    boolean deletePriorCommits = false;
+    InfoStream out = null;
+    String dirImpl = null;
+    int i = 0;
+    while (i<args.length) {
+      String arg = args[i];
+      if ("-delete-prior-commits".equals(arg)) {
+        deletePriorCommits = true;
+      } else if ("-verbose".equals(arg)) {
+        out = new PrintStreamInfoStream(System.out);
+      } else if ("-dir-impl".equals(arg)) {
+        if (i == args.length - 1) {
+          System.out.println("ERROR: missing value for -dir-impl option");
+          System.exit(1);
+        }
+        i++;
+        dirImpl = args[i];
+      } else if (path == null) {
+        path = arg;
+      }else {
+        printUsage();
+      }
+      i++;
+    }
+    if (path == null) {
+      printUsage();
+    }
+    
+    Path p = Paths.get(path);
+    Directory dir = null;
+    if (dirImpl == null) {
+      dir = FSDirectory.open(p);
+    } else {
+      dir = CommandLineUtil.newFSDirectory(dirImpl, p);
+    }
+    return new IndexUpgrader(dir, out, deletePriorCommits);
+  }
+  
+  private final Directory dir;
+  private final IndexWriterConfig iwc;
+  private final boolean deletePriorCommits;
+  
+  /** Creates index upgrader on the given directory.
+   * The tool refuses to upgrade indexes with multiple commit points. */
+  public IndexUpgrader(Directory dir) {
+    this(dir, new IndexWriterConfig(null), false);
+  }
+  
+  /** Creates index upgrader on the given directory. You have the possibility to upgrade indexes
+   * with multiple commit points by removing all older ones. If {@code infoStream} is not {@code null},
+   * all logging output will be sent to this stream. */
+  public IndexUpgrader(Directory dir, InfoStream infoStream, boolean deletePriorCommits) {
+    this(dir, new IndexWriterConfig(null), deletePriorCommits);
+    if (null != infoStream) {
+      this.iwc.setInfoStream(infoStream);
+    }
+  }
+  
+  /** Creates index upgrader on the given directory using the given
+   * config. You have the possibility to upgrade indexes with multiple commit points by removing
+   * all older ones. */
+  public IndexUpgrader(Directory dir, IndexWriterConfig iwc, boolean deletePriorCommits) {
+    this.dir = dir;
+    this.iwc = iwc;
+    this.deletePriorCommits = deletePriorCommits;
+  }
+
+  /** Perform the upgrade. */
+  public void upgrade() throws IOException {
+    if (!DirectoryReader.indexExists(dir)) {
+      throw new IndexNotFoundException(dir.toString());
+    }
+  
+    if (!deletePriorCommits) {
+      final Collection<IndexCommit> commits = DirectoryReader.listCommits(dir);
+      if (commits.size() > 1) {
+        throw new IllegalArgumentException("This tool was invoked to not delete prior commit points, but the following commits were found: " + commits);
+      }
+    }
+
+    if (!(iwc.getCodec() instanceof EncryptedLucene60Codec)) {
+
+    }
+    long cipherVersion = ((EncryptedLucene60Codec) iwc.getCodec()).getCipherFactory().getEncipherVersion();
+    
+    iwc.setMergePolicy(new CipherUpgradeIndexMergePolicy(iwc.getMergePolicy(), cipherVersion));
+    iwc.setIndexDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy());
+    
+    try (final IndexWriter w = new IndexWriter(dir, iwc)) {
+      InfoStream infoStream = iwc.getInfoStream();
+      if (infoStream.isEnabled(LOG_PREFIX)) {
+        infoStream.message(LOG_PREFIX, "Encrypting all segments of index directory '" + dir + "' with cipher version " + cipherVersion + "...");
+      }
+      w.forceMerge(1);
+      if (infoStream.isEnabled(LOG_PREFIX)) {
+        infoStream.message(LOG_PREFIX, "All segments encrypted with cipher version " + cipherVersion);
+        infoStream.message(LOG_PREFIX, "Enforcing commit to rewrite all index metadata...");
+      }
+      w.setCommitData(w.getCommitData()); // fake change to enforce a commit (e.g. if index has no segments)
+      assert w.hasUncommittedChanges();
+      w.commit();
+      if (infoStream.isEnabled(LOG_PREFIX)) {
+        infoStream.message(LOG_PREFIX, "Committed upgraded metadata to index.");
+      }
+    }
+  }
+
+  /**
+   * An {@link UpgradeIndexMergePolicy} that will upgrade all segments using an old cipher version to the
+   * latest cipher version.
+   */
+  private static final class CipherUpgradeIndexMergePolicy extends UpgradeIndexMergePolicy {
+
+    private final long cipherVersion;
+
+    /**
+     * Wrap the given {@link MergePolicy} and intercept forceMerge requests to
+     * only upgrade segments written with previous cipher versions than {@code cipherVersion}.
+     */
+    public CipherUpgradeIndexMergePolicy(MergePolicy base, long cipherVersion) {
+      super(base);
+      this.cipherVersion = cipherVersion;
+    }
+
+    /**
+     * Returns true if the given segment should be re-encrypted. It uses the {@link CipherFactory#CIPHER_VERSION_KEY}
+     * attribute stored in the {@link org.apache.lucene.index.SegmentInfo} to determine if the segment uses
+     * an old version of the cipher.
+     */
+    protected boolean shouldUpgradeSegment(SegmentCommitInfo si) {
+      String value = si.info.getAttribute(CipherFactory.CIPHER_VERSION_KEY);
+      if (value == null) {
+        throw new IllegalStateException("missing value for " + CipherFactory.CIPHER_VERSION_KEY +
+            " for segment: " + si.info.name);
+      }
+
+      long version = Long.parseLong(value);
+      return cipherVersion != version;
+    }
+
+  }
+  
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/AutoPrefixTermsWriter.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/AutoPrefixTermsWriter.java
new file mode 100644
index 0000000..e30cbb7
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/AutoPrefixTermsWriter.java
@@ -0,0 +1,438 @@
+package org.apache.lucene.codecs.encrypted.blocktree;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.index.FilteredTermsEnum;
+import org.apache.lucene.index.Terms;
+import org.apache.lucene.index.TermsEnum;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.BytesRefBuilder;
+import org.apache.lucene.util.StringHelper;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+// TODO: instead of inlining auto-prefix terms with normal terms,
+// we could write them into their own virtual/private field.  This
+// would make search time a bit more complex, since we'd need to
+// merge sort between two TermEnums, but it would also make stats
+// API (used by CheckIndex -verbose) easier to implement since we could
+// just walk this virtual field and gather its stats)
+
+/** Used in the first pass when writing a segment to locate
+ *  "appropriate" auto-prefix terms to pre-compile into the index.
+ *  This visits every term in the index to find prefixes that
+ *  match {@code >= min} and {@code <= max} number of terms. */
+
+class AutoPrefixTermsWriter {
+
+  //static boolean DEBUG = BlockTreeTermsWriter.DEBUG;
+  //static boolean DEBUG = false;
+  //static boolean DEBUG2 = BlockTreeTermsWriter.DEBUG2;
+  //static boolean DEBUG2 = true;
+
+  /** Describes a range of term-space to match, either a simple prefix
+   *  (foo*) or a floor-block range of a prefix (e.g. foo[a-m]*,
+   *  foo[n-z]*) when there are too many terms starting with foo*. */
+  public static final class PrefixTerm implements Comparable<PrefixTerm> {
+    /** Common prefix */
+    public final byte[] prefix;
+
+    /** If this is -2, this is a normal prefix (foo *), else it's the minimum lead byte of the suffix (e.g. 'd' in foo[d-m]*). */
+    public final int floorLeadStart;
+
+    /** The lead byte (inclusive) of the suffix for the term range we match (e.g. 'm' in foo[d-m*]); this is ignored when
+     *  floorLeadStart is -2. */
+    public final int floorLeadEnd;
+
+    public final BytesRef term;
+
+    /** Sole constructor. */
+    public PrefixTerm(byte[] prefix, int floorLeadStart, int floorLeadEnd) {
+      this.prefix = prefix;
+      this.floorLeadStart = floorLeadStart;
+      this.floorLeadEnd = floorLeadEnd;
+      this.term = toBytesRef(prefix, floorLeadStart);
+
+      assert floorLeadEnd >= floorLeadStart;
+      assert floorLeadEnd >= 0;
+      assert floorLeadStart == -2 || floorLeadStart >= 0;
+
+      // We should never create empty-string prefix term:
+      assert prefix.length > 0 || floorLeadStart != -2 || floorLeadEnd != 0xff;
+    }
+
+    @Override
+    public String toString() {
+      String s = brToString(new BytesRef(prefix));
+      if (floorLeadStart == -2) {
+        s += "[-" + Integer.toHexString(floorLeadEnd) + "]";
+      } else {
+        s += "[" + Integer.toHexString(floorLeadStart) + "-" + Integer.toHexString(floorLeadEnd) + "]";
+      }
+      return s;
+    }
+
+    @Override
+    public int compareTo(PrefixTerm other) {
+      int cmp = term.compareTo(other.term);
+      if (cmp == 0) {
+        if (prefix.length != other.prefix.length) {
+          return prefix.length - other.prefix.length;
+        }
+
+        // On tie, sort the bigger floorLeadEnd, earlier, since it
+        // spans more terms, so during intersect, we want to encounter this one
+        // first so we can use it if the automaton accepts the larger range:
+        cmp = other.floorLeadEnd - floorLeadEnd;
+      }
+
+      return cmp;
+    }
+
+    /** Returns the leading term for this prefix term, e.g. "foo" (for
+     *  the foo* prefix) or "foom" (for the foo[m-z]* case). */
+    private static BytesRef toBytesRef(byte[] prefix, int floorLeadStart) {
+      BytesRef br;
+      if (floorLeadStart != -2) {
+        assert floorLeadStart >= 0;
+        br = new BytesRef(prefix.length+1);
+      } else {
+        br = new BytesRef(prefix.length);
+      }
+      System.arraycopy(prefix, 0, br.bytes, 0, prefix.length);
+      br.length = prefix.length;
+      if (floorLeadStart != -2) {
+        assert floorLeadStart >= 0;
+        br.bytes[br.length++] = (byte) floorLeadStart;
+      }
+
+      return br;
+    }
+
+    public int compareTo(BytesRef term) {
+      return this.term.compareTo(term);
+    }
+
+    public TermsEnum getTermsEnum(TermsEnum in) {
+
+      final BytesRef prefixRef = new BytesRef(prefix);
+
+      return new FilteredTermsEnum(in) {
+          {
+            setInitialSeekTerm(term);
+          }
+
+          @Override
+          protected AcceptStatus accept(BytesRef term) {
+            if (StringHelper.startsWith(term, prefixRef) &&
+                (floorLeadEnd == -1 || term.length == prefixRef.length || (term.bytes[term.offset + prefixRef.length] & 0xff) <= floorLeadEnd)) {
+              return AcceptStatus.YES;
+            } else {
+              return AcceptStatus.END;
+            }
+          }
+        };
+    }
+  }
+
+  // for debugging
+  static String brToString(BytesRef b) {
+    try {
+      return b.utf8ToString() + " " + b;
+    } catch (Throwable t) {
+      // If BytesRef isn't actually UTF8, or it's eg a
+      // prefix of UTF8 that ends mid-unicode-char, we
+      // fallback to hex:
+      return b.toString();
+    }
+  }
+
+  final List<PrefixTerm> prefixes = new ArrayList<>();
+  private final int minItemsInPrefix;
+  private final int maxItemsInPrefix;
+
+  // Records index into pending where the current prefix at that
+  // length "started"; for example, if current term starts with 't',
+  // startsByPrefix[0] is the index into pending for the first
+  // term/sub-block starting with 't'.  We use this to figure out when
+  // to write a new block:
+  private final BytesRefBuilder lastTerm = new BytesRefBuilder();
+  private int[] prefixStarts = new int[8];
+  private List<Object> pending = new ArrayList<>();
+
+  //private final String segment;
+
+  public AutoPrefixTermsWriter(Terms terms, int minItemsInPrefix, int maxItemsInPrefix) throws IOException {
+    this.minItemsInPrefix = minItemsInPrefix;
+    this.maxItemsInPrefix = maxItemsInPrefix;
+    //this.segment = segment;
+
+    TermsEnum termsEnum = terms.iterator();
+    while (true) {
+      BytesRef term = termsEnum.next();
+      if (term == null) {
+        break;
+      }
+      //if (DEBUG) System.out.println("pushTerm: " + brToString(term));
+      pushTerm(term);
+    }
+
+    if (pending.size() > 1) {
+      pushTerm(EncryptedBlockTreeTermsWriter.EMPTY_BYTES_REF);
+
+      // Also maybe save floor prefixes in root block; this can be a biggish perf gain for large ranges:
+      /*
+      System.out.println("root block pending.size=" + pending.size());
+      for(Object o : pending) {
+        System.out.println("  " + o);
+      }
+      */
+      while (pending.size() >= minItemsInPrefix) {
+        savePrefixes(0, pending.size());
+      }
+    }
+
+    // Even though we visited terms in already-sorted order, the prefixes
+    // can be slightly unsorted, e.g. aaaaa will be before aaa, so we
+    // must sort here so our caller can do merge sort into actual terms
+    // when writing.  Probably we should use CollectionUtil.timSort here?
+    Collections.sort(prefixes);
+  }
+
+  /** Pushes the new term to the top of the stack, and writes new blocks. */
+  private void pushTerm(BytesRef text) throws IOException {
+    int limit = Math.min(lastTerm.length(), text.length);
+    //if (DEBUG) System.out.println("\nterm: " + text.utf8ToString());
+
+    // Find common prefix between last term and current term:
+    int pos = 0;
+    while (pos < limit && lastTerm.byteAt(pos) == text.bytes[text.offset+pos]) {
+      pos++;
+    }
+
+    //if (DEBUG) System.out.println("  shared=" + pos + "  lastTerm.length=" + lastTerm.length());
+
+    // Close the "abandoned" suffix now:
+    for(int i=lastTerm.length()-1;i>=pos;i--) {
+
+      // How many items on top of the stack share the current suffix
+      // we are closing:
+      int prefixTopSize = pending.size() - prefixStarts[i];
+
+      while (prefixTopSize >= minItemsInPrefix) {       
+        //if (DEBUG) System.out.println("  pop: i=" + i + " prefixTopSize=" + prefixTopSize + " minItemsInBlock=" + minItemsInPrefix);
+        savePrefixes(i+1, prefixTopSize);
+        //prefixStarts[i] -= prefixTopSize;
+        //if (DEBUG) System.out.println("    after savePrefixes: " + (pending.size() - prefixStarts[i]) + " pending.size()=" + pending.size() + " start=" + prefixStarts[i]);
+
+        // For large floor blocks, it's possible we should now re-run on the new prefix terms we just created:
+        prefixTopSize = pending.size() - prefixStarts[i];
+      }
+    }
+
+    if (prefixStarts.length < text.length) {
+      prefixStarts = ArrayUtil.grow(prefixStarts, text.length);
+    }
+
+    // Init new tail:
+    for(int i=pos;i<text.length;i++) {
+      prefixStarts[i] = pending.size();
+    }
+
+    lastTerm.copyBytes(text);
+
+    // Only append the first (optional) empty string, no the fake last one used to close all prefixes:
+    if (text.length > 0 || pending.isEmpty()) {
+      byte[] termBytes = new byte[text.length];
+      System.arraycopy(text.bytes, text.offset, termBytes, 0, text.length);
+      pending.add(termBytes);
+    }
+  }
+  
+  void savePrefixes(int prefixLength, int count) throws IOException {
+
+    assert count > 0;
+
+    /*
+    if (DEBUG2) {
+      BytesRef br = new BytesRef(lastTerm.bytes());
+      br.length = prefixLength;
+      //System.out.println("  savePrefixes: seg=" + segment + " " + brToString(br) + " count=" + count + " pending.size()=" + pending.size());
+      System.out.println("  savePrefixes: " + brToString(br) + " count=" + count + " pending.size()=" + pending.size());
+    }
+    */
+
+    int lastSuffixLeadLabel = -2;
+
+    int start = pending.size()-count;
+    assert start >=0;
+
+    // Special case empty-string suffix case: we are being asked to build prefix terms for all aaa* terms, but 
+    // the exact term aaa is here, and we must skip it (it is handled "higher", under the aa* terms):
+    Object o = pending.get(start);
+    boolean skippedEmptyStringSuffix = false;
+    if (o instanceof byte[]) {
+      if (((byte[]) o).length == prefixLength) {
+        start++;
+        count--;
+        //if (DEBUG) System.out.println("  skip empty-string term suffix");
+        skippedEmptyStringSuffix = true;
+      }
+    } else {
+      PrefixTerm prefix = (PrefixTerm) o;
+      if (prefix.term.bytes.length == prefixLength) {
+        start++;
+        count--;
+        //if (DEBUG) System.out.println("  skip empty-string PT suffix");
+        skippedEmptyStringSuffix = true;
+      }
+    }
+
+    int end = pending.size();
+    int nextBlockStart = start;
+    int nextFloorLeadLabel = -1;
+    int prefixCount = 0;
+
+    PrefixTerm lastPTEntry = null;
+
+    for (int i=start; i<end; i++) {
+
+      byte[] termBytes;
+      o = pending.get(i);
+      PrefixTerm ptEntry;
+      if (o instanceof byte[]) {
+        ptEntry = null;
+        termBytes = (byte[]) o;
+      } else {
+        ptEntry = (PrefixTerm) o;
+        termBytes = ptEntry.term.bytes;
+        if (ptEntry.prefix.length != prefixLength) {
+          assert ptEntry.prefix.length > prefixLength;
+          ptEntry = null;
+        }
+      }
+
+      //if (DEBUG) System.out.println("    check term=" + brToString(new BytesRef(termBytes)) + " o=" + o);
+
+      // We handled the empty-string suffix case up front:
+      assert termBytes.length > prefixLength;
+
+      int suffixLeadLabel = termBytes[prefixLength] & 0xff;
+
+      //if (DEBUG) System.out.println("  i=" + i + " o=" + o + " suffixLeadLabel=" + Integer.toHexString(suffixLeadLabel) + " pendingCount=" + (i - nextBlockStart) + " min=" + minItemsInPrefix);
+
+      if (suffixLeadLabel != lastSuffixLeadLabel) {
+        // This is a boundary, a chance to make an auto-prefix term if we want:
+
+        // When we are "recursing" (generating auto-prefix terms on a block of
+        // floor'd auto-prefix terms), this assert is non-trivial because it
+        // ensures the floorLeadEnd of the previous terms is in fact less
+        // than the lead start of the current entry:
+        assert suffixLeadLabel > lastSuffixLeadLabel: "suffixLeadLabel=" + suffixLeadLabel + " vs lastSuffixLeadLabel=" + lastSuffixLeadLabel;
+
+        int itemsInBlock = i - nextBlockStart;
+
+        if (itemsInBlock >= minItemsInPrefix && end-nextBlockStart > maxItemsInPrefix) {
+          // The count is too large for one block, so we must break it into "floor" blocks, where we record
+          // the leading label of the suffix of the first term in each floor block, so at search time we can
+          // jump to the right floor block.  We just use a naive greedy segmenter here: make a new floor
+          // block as soon as we have at least minItemsInBlock.  This is not always best: it often produces
+          // a too-small block as the final block:
+
+          // If the last entry was another prefix term of the same length, then it represents a range of terms, so we must use its ending
+          // prefix label as our ending label:
+          if (lastPTEntry != null) {
+            //if (DEBUG) System.out.println("  use last");
+            lastSuffixLeadLabel = lastPTEntry.floorLeadEnd;
+          }
+          savePrefix(prefixLength, nextFloorLeadLabel, lastSuffixLeadLabel);
+
+          prefixCount++;
+          nextFloorLeadLabel = suffixLeadLabel;
+          nextBlockStart = i;
+        }
+
+        if (nextFloorLeadLabel == -1) {
+          nextFloorLeadLabel = suffixLeadLabel;
+          //if (DEBUG) System.out.println("set first lead label=" + nextFloorLeadLabel);
+        }
+
+        lastSuffixLeadLabel = suffixLeadLabel;
+      }
+
+      lastPTEntry = ptEntry;
+    }
+
+    // Write last block, if any:
+    if (nextBlockStart < end) {
+      //System.out.println("  lastPTEntry=" + lastPTEntry + " lastSuffixLeadLabel=" + lastSuffixLeadLabel);
+      if (lastPTEntry != null) {
+        lastSuffixLeadLabel = lastPTEntry.floorLeadEnd;
+      }
+      assert lastSuffixLeadLabel >= nextFloorLeadLabel: "lastSuffixLeadLabel=" + lastSuffixLeadLabel + " nextFloorLeadLabel=" + nextFloorLeadLabel;
+      if (prefixCount == 0) {
+        if (prefixLength > 0) {
+          savePrefix(prefixLength, -2, 0xff);
+          prefixCount++;
+          
+          // If we skipped empty string suffix, e.g. term aaa for prefix aaa*, since we
+          // are now writing the full aaa* prefix term, we include it here:
+          if (skippedEmptyStringSuffix) {
+            count++;
+          }
+        } else {
+          // Don't add a prefix term for all terms in the index!
+        }
+      } else {
+        if (lastSuffixLeadLabel == -2) {
+          // Special case when closing the empty string root block:
+          lastSuffixLeadLabel = 0xff;
+        }
+        savePrefix(prefixLength, nextFloorLeadLabel, lastSuffixLeadLabel);
+        prefixCount++;
+      }
+    }
+
+    // Remove slice from the top of the pending stack, that we just wrote:
+
+    pending.subList(pending.size()-count, pending.size()).clear();
+
+    // Append prefix terms for each prefix, since these count like real terms that also need to be "rolled up":
+    for(int i=0;i<prefixCount;i++) {
+      PrefixTerm pt = prefixes.get(prefixes.size()-(prefixCount-i));
+      pending.add(pt);
+    }
+  }
+
+  private void savePrefix(int prefixLength, int floorLeadStart, int floorLeadEnd) {
+    byte[] prefix = new byte[prefixLength];
+    System.arraycopy(lastTerm.bytes(), 0, prefix, 0, prefixLength);
+    assert floorLeadStart != -1;
+    assert floorLeadEnd != -1;
+
+    PrefixTerm pt = new PrefixTerm(prefix, floorLeadStart, floorLeadEnd); 
+    //if (DEBUG2) System.out.println("    savePrefix: seg=" + segment + " " + pt + " count=" + count);
+    //if (DEBUG) System.out.println("    savePrefix: " + pt);
+
+    prefixes.add(pt);
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/BitSetPostingsEnum.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/BitSetPostingsEnum.java
new file mode 100644
index 0000000..6b2c69e
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/BitSetPostingsEnum.java
@@ -0,0 +1,95 @@
+package org.apache.lucene.codecs.encrypted.blocktree;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.index.PostingsEnum;
+import org.apache.lucene.search.DocIdSetIterator;
+import org.apache.lucene.util.BitSet;
+import org.apache.lucene.util.BitSetIterator;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.FixedBitSet;
+
+import java.io.IOException;
+
+/** Takes a {@link FixedBitSet} and creates a DOCS {@link PostingsEnum} from it. */
+
+class BitSetPostingsEnum extends PostingsEnum {
+  private final BitSet bits;
+  private DocIdSetIterator in;
+  
+  BitSetPostingsEnum(BitSet bits) {
+    this.bits = bits;
+    reset();
+  }
+
+  @Override
+  public int freq() throws IOException {
+    return 1;
+  }
+
+  @Override
+  public int docID() {
+    if (in == null) {
+      return -1;
+    } else {
+      return in.docID();
+    }
+  }
+
+  @Override
+  public int nextDoc() throws IOException {
+    if (in == null) {
+      in = new BitSetIterator(bits, 0);
+    }
+    return in.nextDoc();
+  }
+
+  @Override
+  public int advance(int target) throws IOException {
+    return in.advance(target);
+  }
+
+  @Override
+  public long cost() {
+    return in.cost();
+  }
+  
+  void reset() {
+    in = null;
+  }
+
+  @Override
+  public BytesRef getPayload() {
+    return null;
+  }
+
+  @Override
+  public int nextPosition() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public int startOffset() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public int endOffset() {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/BitSetTermsEnum.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/BitSetTermsEnum.java
new file mode 100644
index 0000000..b03b648
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/BitSetTermsEnum.java
@@ -0,0 +1,83 @@
+package org.apache.lucene.codecs.encrypted.blocktree;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.PostingsWriterBase;
+import org.apache.lucene.index.PostingsEnum;
+import org.apache.lucene.index.TermsEnum;
+import org.apache.lucene.util.BitSet;
+import org.apache.lucene.util.BytesRef;
+
+/** Silly stub class, used only when writing an auto-prefix
+ *  term in order to expose DocsEnum over a FixedBitSet.  We
+ *  pass this to {@link PostingsWriterBase#writeTerm} so 
+ *  that it can pull .docs() multiple times for the
+ *  current term. */
+
+class BitSetTermsEnum extends TermsEnum {
+  private final BitSetPostingsEnum postingsEnum;
+
+  public BitSetTermsEnum(BitSet docs) {
+    postingsEnum = new BitSetPostingsEnum(docs);
+  }
+
+  @Override
+  public SeekStatus seekCeil(BytesRef text) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void seekExact(long ord) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public BytesRef term() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public BytesRef next() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public long ord() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public int docFreq() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public long totalTermFreq() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public PostingsEnum postings(PostingsEnum reuse, int flags) {
+    if (flags != PostingsEnum.NONE) {
+      // We only work with DOCS_ONLY fields
+      return null;
+    }
+    postingsEnum.reset();
+    return postingsEnum;
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedBlockTreeTermsReader.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedBlockTreeTermsReader.java
new file mode 100644
index 0000000..7bf02f3
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedBlockTreeTermsReader.java
@@ -0,0 +1,354 @@
+package org.apache.lucene.codecs.encrypted.blocktree;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.CodecUtil;
+import org.apache.lucene.codecs.FieldsProducer;
+import org.apache.lucene.codecs.PostingsReaderBase;
+import org.apache.lucene.codecs.blocktree.BlockTreeTermsWriter;
+import org.apache.lucene.codecs.encrypted.CipherFactory;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.FieldInfo;
+import org.apache.lucene.index.IndexFileNames;
+import org.apache.lucene.index.IndexOptions;
+import org.apache.lucene.index.SegmentReadState;
+import org.apache.lucene.index.Terms;
+import org.apache.lucene.search.PrefixQuery;
+import org.apache.lucene.search.TermRangeQuery;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.Accountables;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.IOUtils;
+import org.apache.lucene.util.fst.ByteSequenceOutputs;
+import org.apache.lucene.util.fst.Outputs;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TreeMap;
+
+/** A block-based terms index and dictionary that assigns
+ *  terms to variable length blocks according to how they
+ *  share prefixes.  The terms index is a prefix trie
+ *  whose leaves are term blocks.  The advantage of this
+ *  approach is that seekExact is often able to
+ *  determine a term cannot exist without doing any IO, and
+ *  intersection with Automata is very fast.  Note that this
+ *  terms dictionary has its own fixed terms index (ie, it
+ *  does not support a pluggable terms index
+ *  implementation).
+ *
+ *  <p><b>NOTE</b>: this terms dictionary supports
+ *  min/maxItemsPerBlock during indexing to control how
+ *  much memory the terms index uses.</p>
+ *
+ *  <p>If auto-prefix terms were indexed (see
+ *  {@link BlockTreeTermsWriter}), then the {@link Terms#intersect}
+ *  implementation here will make use of these terms only if the
+ *  automaton has a binary sink state, i.e. an accept state
+ *  which has a transition to itself accepting all byte values.
+ *  For example, both {@link PrefixQuery} and {@link TermRangeQuery}
+ *  pass such automata to {@link Terms#intersect}.</p>
+ *
+ *  <p>The data structure used by this implementation is very
+ *  similar to a burst trie
+ *  (http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.18.3499),
+ *  but with added logic to break up too-large blocks of all
+ *  terms sharing a given prefix into smaller ones.</p>
+ *
+ *  <p>Use {@link org.apache.lucene.index.CheckIndex} with the <code>-verbose</code>
+ *  option to see summary statistics on the blocks in the
+ *  dictionary.
+ *
+ *  See {@link BlockTreeTermsWriter}.
+ *
+ * @lucene.experimental
+ */
+
+public final class EncryptedBlockTreeTermsReader extends FieldsProducer {
+
+  static final Outputs<BytesRef> FST_OUTPUTS = ByteSequenceOutputs.getSingleton();
+
+  static final BytesRef NO_OUTPUT = FST_OUTPUTS.getNoOutput();
+
+  static final int OUTPUT_FLAGS_NUM_BITS = 2;
+  static final int OUTPUT_FLAGS_MASK = 0x3;
+  static final int OUTPUT_FLAG_IS_FLOOR = 0x1;
+  static final int OUTPUT_FLAG_HAS_TERMS = 0x2;
+
+  /** Extension of terms file */
+  static final String TERMS_EXTENSION = "tim";
+  final static String TERMS_CODEC_NAME = "BlockTreeTermsDict";
+
+  /** Initial terms format. */
+  public static final int VERSION_START = 0;
+
+  /** Auto-prefix terms. */
+  public static final int VERSION_AUTO_PREFIX_TERMS = 1;
+
+  /** Conditional auto-prefix terms: we record at write time whether
+   *  this field did write any auto-prefix terms. */
+  public static final int VERSION_AUTO_PREFIX_TERMS_COND = 2;
+
+  /** Current terms format. */
+  public static final int VERSION_CURRENT = VERSION_AUTO_PREFIX_TERMS_COND;
+
+  /** Extension of terms index file */
+  static final String TERMS_INDEX_EXTENSION = "tip";
+  final static String TERMS_INDEX_CODEC_NAME = "BlockTreeTermsIndex";
+
+  // Open input to the main terms dict file (_X.tib)
+  final IndexInput termsIn;
+
+  //private static final boolean DEBUG = BlockTreeTermsWriter.DEBUG;
+
+  final long keyVersion;
+  final CipherFactory cipherFactory;
+
+  // Reads the terms dict entries, to gather state to
+  // produce DocsEnum on demand
+  final PostingsReaderBase postingsReader;
+
+  private final TreeMap<String,EncryptedFieldReader> fields = new TreeMap<>();
+
+  /** File offset where the directory starts in the terms file. */
+  private long dirOffset;
+
+  /** File offset where the directory starts in the index file. */
+  private long indexDirOffset;
+
+  final String segment;
+
+  final int version;
+
+  final boolean anyAutoPrefixTerms;
+
+  /** Sole constructor. */
+  public EncryptedBlockTreeTermsReader(CipherFactory cipherFactory, PostingsReaderBase postingsReader, SegmentReadState state) throws IOException {
+    boolean success = false;
+    IndexInput indexIn = null;
+
+    this.postingsReader = postingsReader;
+    this.segment = state.segmentInfo.name;
+    
+    String termsName = IndexFileNames.segmentFileName(segment, state.segmentSuffix, TERMS_EXTENSION);
+    try {
+      termsIn = state.directory.openInput(termsName, state.context);
+      version = CodecUtil.checkIndexHeader(termsIn, TERMS_CODEC_NAME, VERSION_START, VERSION_CURRENT, state.segmentInfo.getId(), state.segmentSuffix);
+
+      if (version < VERSION_AUTO_PREFIX_TERMS) {
+        // Old (pre-5.2.0) index, no auto-prefix terms:
+        this.anyAutoPrefixTerms = false;
+      } else if (version == VERSION_AUTO_PREFIX_TERMS) {
+        // 5.2.x index, might have auto-prefix terms:
+        this.anyAutoPrefixTerms = true;
+      } else {
+        // 5.3.x index, we record up front if we may have written any auto-prefix terms:
+        assert version >= VERSION_AUTO_PREFIX_TERMS_COND;
+        byte b = termsIn.readByte();
+        if (b == 0) {
+          this.anyAutoPrefixTerms = false;
+        } else if (b == 1) {
+          this.anyAutoPrefixTerms = true;
+        } else {
+          throw new CorruptIndexException("invalid anyAutoPrefixTerms: expected 0 or 1 but got " + b, termsIn);
+        }
+      }
+
+      String indexName = IndexFileNames.segmentFileName(segment, state.segmentSuffix, TERMS_INDEX_EXTENSION);
+      indexIn = state.directory.openInput(indexName, state.context);
+      CodecUtil.checkIndexHeader(indexIn, TERMS_INDEX_CODEC_NAME, version, version, state.segmentInfo.getId(), state.segmentSuffix);
+      CodecUtil.checksumEntireFile(indexIn);
+
+      // Read the key version from the segment info
+      keyVersion = Long.parseLong(state.segmentInfo.getAttribute(CipherFactory.CIPHER_VERSION_KEY));
+      // Keep a reference to the cipher factory. Cipher will be instantiated by the term enum for thread-safety.
+      this.cipherFactory = cipherFactory;
+
+      // Have PostingsReader init itself
+      postingsReader.init(termsIn, state);
+      
+      // NOTE: data file is too costly to verify checksum against all the bytes on open,
+      // but for now we at least verify proper structure of the checksum footer: which looks
+      // for FOOTER_MAGIC + algorithmID. This is cheap and can detect some forms of corruption
+      // such as file truncation.
+      CodecUtil.retrieveChecksum(termsIn);
+
+      // Read per-field details
+      seekDir(termsIn, dirOffset);
+      seekDir(indexIn, indexDirOffset);
+
+      final int numFields = termsIn.readVInt();
+      if (numFields < 0) {
+        throw new CorruptIndexException("invalid numFields: " + numFields, termsIn);
+      }
+
+      for (int i = 0; i < numFields; ++i) {
+        final int field = termsIn.readVInt();
+        final long numTerms = termsIn.readVLong();
+        if (numTerms <= 0) {
+          throw new CorruptIndexException("Illegal numTerms for field number: " + field, termsIn);
+        }
+        final int numBytes = termsIn.readVInt();
+        if (numBytes < 0) {
+          throw new CorruptIndexException("invalid rootCode for field number: " + field + ", numBytes=" + numBytes, termsIn);
+        }
+        final BytesRef rootCode = new BytesRef(new byte[numBytes]);
+        termsIn.readBytes(rootCode.bytes, 0, numBytes);
+        rootCode.length = numBytes;
+        final FieldInfo fieldInfo = state.fieldInfos.fieldInfo(field);
+        if (fieldInfo == null) {
+          throw new CorruptIndexException("invalid field number: " + field, termsIn);
+        }
+        final long sumTotalTermFreq = fieldInfo.getIndexOptions() == IndexOptions.DOCS ? -1 : termsIn.readVLong();
+        final long sumDocFreq = termsIn.readVLong();
+        final int docCount = termsIn.readVInt();
+        final int longsSize = termsIn.readVInt();
+        if (longsSize < 0) {
+          throw new CorruptIndexException("invalid longsSize for field: " + fieldInfo.name + ", longsSize=" + longsSize, termsIn);
+        }
+        BytesRef minTerm = readBytesRef(termsIn);
+        BytesRef maxTerm = readBytesRef(termsIn);
+        if (docCount < 0 || docCount > state.segmentInfo.maxDoc()) { // #docs with field must be <= #docs
+          throw new CorruptIndexException("invalid docCount: " + docCount + " maxDoc: " + state.segmentInfo.maxDoc(), termsIn);
+        }
+        if (sumDocFreq < docCount) {  // #postings must be >= #docs with field
+          throw new CorruptIndexException("invalid sumDocFreq: " + sumDocFreq + " docCount: " + docCount, termsIn);
+        }
+        if (sumTotalTermFreq != -1 && sumTotalTermFreq < sumDocFreq) { // #positions must be >= #postings
+          throw new CorruptIndexException("invalid sumTotalTermFreq: " + sumTotalTermFreq + " sumDocFreq: " + sumDocFreq, termsIn);
+        }
+        final long indexStartFP = indexIn.readVLong();
+        EncryptedFieldReader previous = fields.put(fieldInfo.name,
+                                          new EncryptedFieldReader(this, fieldInfo, numTerms, rootCode, sumTotalTermFreq,
+                                                                   sumDocFreq, docCount, indexStartFP, longsSize, indexIn,
+                                                                   minTerm, maxTerm));
+        if (previous != null) {
+          throw new CorruptIndexException("duplicate field: " + fieldInfo.name, termsIn);
+        }
+      }
+      
+      indexIn.close();
+      success = true;
+    } finally {
+      if (!success) {
+        // this.close() will close in:
+        IOUtils.closeWhileHandlingException(indexIn, this);
+      }
+    }
+  }
+
+  private static BytesRef readBytesRef(IndexInput in) throws IOException {
+    BytesRef bytes = new BytesRef();
+    bytes.length = in.readVInt();
+    bytes.bytes = new byte[bytes.length];
+    in.readBytes(bytes.bytes, 0, bytes.length);
+    return bytes;
+  }
+
+  /** Seek {@code input} to the directory offset. */
+  private void seekDir(IndexInput input, long dirOffset)
+      throws IOException {
+    input.seek(input.length() - CodecUtil.footerLength() - 8);
+    dirOffset = input.readLong();
+    input.seek(dirOffset);
+  }
+
+  // for debugging
+  // private static String toHex(int v) {
+  //   return "0x" + Integer.toHexString(v);
+  // }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      IOUtils.close(termsIn, postingsReader);
+    } finally { 
+      // Clear so refs to terms index is GCable even if
+      // app hangs onto us:
+      fields.clear();
+    }
+  }
+
+  @Override
+  public Iterator<String> iterator() {
+    return Collections.unmodifiableSet(fields.keySet()).iterator();
+  }
+
+  @Override
+  public Terms terms(String field) throws IOException {
+    assert field != null;
+    return fields.get(field);
+  }
+
+  @Override
+  public int size() {
+    return fields.size();
+  }
+
+  // for debugging
+  String brToString(BytesRef b) {
+    if (b == null) {
+      return "null";
+    } else {
+      try {
+        return b.utf8ToString() + " " + b;
+      } catch (Throwable t) {
+        // If BytesRef isn't actually UTF8, or it's eg a
+        // prefix of UTF8 that ends mid-unicode-char, we
+        // fallback to hex:
+        return b.toString();
+      }
+    }
+  }
+
+  @Override
+  public long ramBytesUsed() {
+    long sizeInBytes = postingsReader.ramBytesUsed();
+    for(EncryptedFieldReader reader : fields.values()) {
+      sizeInBytes += reader.ramBytesUsed();
+    }
+    return sizeInBytes;
+  }
+
+  @Override
+  public Collection<Accountable> getChildResources() {
+    List<Accountable> resources = new ArrayList<>();
+    resources.addAll(Accountables.namedAccountables("field", fields));
+    resources.add(Accountables.namedAccountable("delegate", postingsReader));
+    return Collections.unmodifiableList(resources);
+  }
+
+  @Override
+  public void checkIntegrity() throws IOException { 
+    // term dictionary
+    CodecUtil.checksumEntireFile(termsIn);
+      
+    // postings
+    postingsReader.checkIntegrity();
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + "(fields=" + fields.size() + ",delegate=" + postingsReader + ")";
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedBlockTreeTermsWriter.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedBlockTreeTermsWriter.java
new file mode 100644
index 0000000..410562a
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedBlockTreeTermsWriter.java
@@ -0,0 +1,1264 @@
+package org.apache.lucene.codecs.encrypted.blocktree;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.BlockTermState;
+import org.apache.lucene.codecs.CodecUtil;
+import org.apache.lucene.codecs.FieldsConsumer;
+import org.apache.lucene.codecs.PostingsWriterBase;
+import org.apache.lucene.codecs.compressing.GrowableByteArrayDataOutput;
+import org.apache.lucene.codecs.encrypted.CipherFactory;
+import org.apache.lucene.index.FieldInfo;
+import org.apache.lucene.index.FieldInfos;
+import org.apache.lucene.index.Fields;
+import org.apache.lucene.index.IndexFileNames;
+import org.apache.lucene.index.IndexOptions;
+import org.apache.lucene.index.PostingsEnum;
+import org.apache.lucene.index.SegmentWriteState;
+import org.apache.lucene.index.Terms;
+import org.apache.lucene.index.TermsEnum;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.store.RAMOutputStream;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.BytesRefBuilder;
+import org.apache.lucene.util.FixedBitSet;
+import org.apache.lucene.util.IOUtils;
+import org.apache.lucene.util.IntsRefBuilder;
+import org.apache.lucene.util.StringHelper;
+import org.apache.lucene.util.fst.Builder;
+import org.apache.lucene.util.fst.ByteSequenceOutputs;
+import org.apache.lucene.util.fst.BytesRefFSTEnum;
+import org.apache.lucene.util.fst.FST;
+import org.apache.lucene.util.fst.Util;
+import org.apache.lucene.util.packed.PackedInts;
+
+import javax.crypto.Cipher;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/*
+  TODO:
+  
+    - Currently there is a one-to-one mapping of indexed
+      term to term block, but we could decouple the two, ie,
+      put more terms into the index than there are blocks.
+      The index would take up more RAM but then it'd be able
+      to avoid seeking more often and could make PK/FuzzyQ
+      faster if the additional indexed terms could store
+      the offset into the terms block.
+
+    - The blocks are not written in true depth-first
+      order, meaning if you just next() the file pointer will
+      sometimes jump backwards.  For example, block foo* will
+      be written before block f* because it finished before.
+      This could possibly hurt performance if the terms dict is
+      not hot, since OSs anticipate sequential file access.  We
+      could fix the writer to re-order the blocks as a 2nd
+      pass.
+
+    - Each block encodes the term suffixes packed
+      sequentially using a separate vInt per term, which is
+      1) wasteful and 2) slow (must linear scan to find a
+      particular suffix).  We should instead 1) make
+      random-access array so we can directly access the Nth
+      suffix, and 2) bulk-encode this array using bulk int[]
+      codecs; then at search time we can binary search when
+      we seek a particular term.
+*/
+
+/**
+ * Block-based terms index and dictionary writer.
+ * <p>
+ * Writes terms dict and index, block-encoding (column
+ * stride) each term's metadata for each set of terms
+ * between two index terms.
+ * <p>
+ *
+ * If {@code minItemsInAutoPrefix} is not zero, then for
+ * {@link IndexOptions#DOCS} fields we detect prefixes that match
+ * "enough" terms and insert auto-prefix terms into the index, which are
+ * used by {@link Terms#intersect}  at search time to speed up prefix
+ * and range queries.  Besides {@link Terms#intersect}, these
+ * auto-prefix terms are invisible to all other APIs (don't change terms
+ * stats, don't show up in normal {@link TermsEnum}s, etc.).
+ * <p>
+ *
+ * Files:
+ * <ul>
+ *   <li><tt>.tim</tt>: <a href="#Termdictionary">Term Dictionary</a></li>
+ *   <li><tt>.tip</tt>: <a href="#Termindex">Term Index</a></li>
+ * </ul>
+ * <p>
+ * <a name="Termdictionary"></a>
+ * <h3>Term Dictionary</h3>
+ *
+ * <p>The .tim file contains the list of terms in each
+ * field along with per-term statistics (such as docfreq)
+ * and per-term metadata (typically pointers to the postings list
+ * for that term in the inverted index).
+ * </p>
+ *
+ * <p>The .tim is arranged in blocks: with blocks containing
+ * a variable number of entries (by default 25-48), where
+ * each entry is either a term or a reference to a
+ * sub-block.</p>
+ *
+ * <p>NOTE: The term dictionary can plug into different postings implementations:
+ * the postings writer/reader are actually responsible for encoding 
+ * and decoding the Postings Metadata and Term Metadata sections.</p>
+ *
+ * <ul>
+ *    <li>TermsDict (.tim) --&gt; Header, HasAutoPrefixTerms, <i>PostingsHeader</i>, NodeBlock<sup>NumBlocks</sup>,
+ *                               FieldSummary, DirOffset, Footer</li>
+ *    <li>NodeBlock --&gt; (OuterNode | InnerNode)</li>
+ *    <li>OuterNode --&gt; EntryCount, SuffixLength, Byte<sup>SuffixLength</sup>, StatsLength, &lt; TermStats &gt;<sup>EntryCount</sup>, MetaLength, &lt;<i>TermMetadata</i>&gt;<sup>EntryCount</sup></li>
+ *    <li>InnerNode --&gt; EntryCount, SuffixLength[,Sub?], Byte<sup>SuffixLength</sup>, StatsLength, &lt; TermStats ? &gt;<sup>EntryCount</sup>, MetaLength, &lt;<i>TermMetadata ? </i>&gt;<sup>EntryCount</sup></li>
+ *    <li>TermStats --&gt; DocFreq, TotalTermFreq </li>
+ *    <li>FieldSummary --&gt; NumFields, &lt;FieldNumber, NumTerms, RootCodeLength, Byte<sup>RootCodeLength</sup>,
+ *                            SumTotalTermFreq?, SumDocFreq, DocCount, LongsSize, MinTerm, MaxTerm&gt;<sup>NumFields</sup></li>
+ *    <li>Header --&gt; {@link CodecUtil#writeHeader CodecHeader}</li>
+ *    <li>DirOffset --&gt; {@link DataOutput#writeLong Uint64}</li>
+ *    <li>MinTerm,MaxTerm --&gt; {@link DataOutput#writeVInt VInt} length followed by the byte[]</li>
+ *    <li>EntryCount,SuffixLength,StatsLength,DocFreq,MetaLength,NumFields,
+ *        FieldNumber,RootCodeLength,DocCount,LongsSize --&gt; {@link DataOutput#writeVInt VInt}</li>
+ *    <li>TotalTermFreq,NumTerms,SumTotalTermFreq,SumDocFreq --&gt; 
+ *        {@link DataOutput#writeVLong VLong}</li>
+ *    <li>Footer --&gt; {@link CodecUtil#writeFooter CodecFooter}</li>
+ * </ul>
+ * <p>Notes:</p>
+ * <ul>
+ *    <li>Header is a {@link CodecUtil#writeHeader CodecHeader} storing the version information
+ *        for the BlockTree implementation.</li>
+ *    <li>HasAutoPrefixTerms is a single byte; 1 means there may be auto-prefix terms and 0 means there are none.
+ *    <li>DirOffset is a pointer to the FieldSummary section.</li>
+ *    <li>DocFreq is the count of documents which contain the term.</li>
+ *    <li>TotalTermFreq is the total number of occurrences of the term. This is encoded
+ *        as the difference between the total number of occurrences and the DocFreq.</li>
+ *    <li>FieldNumber is the fields number from {@link FieldInfos}. (.fnm)</li>
+ *    <li>NumTerms is the number of unique terms for the field.</li>
+ *    <li>RootCode points to the root block for the field.</li>
+ *    <li>SumDocFreq is the total number of postings, the number of term-document pairs across
+ *        the entire field.</li>
+ *    <li>DocCount is the number of documents that have at least one posting for this field.</li>
+ *    <li>LongsSize records how many long values the postings writer/reader record per term
+ *        (e.g., to hold freq/prox/doc file offsets).
+ *    <li>MinTerm, MaxTerm are the lowest and highest term in this field.</li>
+ *    <li>PostingsHeader and TermMetadata are plugged into by the specific postings implementation:
+ *        these contain arbitrary per-file data (such as parameters or versioning information) 
+ *        and per-term data (such as pointers to inverted files).</li>
+ *    <li>For inner nodes of the tree, every entry will steal one bit to mark whether it points
+ *        to child nodes(sub-block). If so, the corresponding TermStats and TermMetaData are omitted </li>
+ * </ul>
+ * <a name="Termindex"></a>
+ * <h3>Term Index</h3>
+ * <p>The .tip file contains an index into the term dictionary, so that it can be 
+ * accessed randomly.  The index is also used to determine
+ * when a given term cannot exist on disk (in the .tim file), saving a disk seek.</p>
+ * <ul>
+ *   <li>TermsIndex (.tip) --&gt; Header, FSTIndex<sup>NumFields</sup>
+ *                                &lt;IndexStartFP&gt;<sup>NumFields</sup>, DirOffset, Footer</li>
+ *   <li>Header --&gt; {@link CodecUtil#writeHeader CodecHeader}</li>
+ *   <li>DirOffset --&gt; {@link DataOutput#writeLong Uint64}</li>
+ *   <li>IndexStartFP --&gt; {@link DataOutput#writeVLong VLong}</li>
+ *   <!-- TODO: better describe FST output here -->
+ *   <li>FSTIndex --&gt; {@link FST FST&lt;byte[]&gt;}</li>
+ *   <li>Footer --&gt; {@link CodecUtil#writeFooter CodecFooter}</li>
+ * </ul>
+ * <p>Notes:</p>
+ * <ul>
+ *   <li>The .tip file contains a separate FST for each
+ *       field.  The FST maps a term prefix to the on-disk
+ *       block that holds all terms starting with that
+ *       prefix.  Each field's IndexStartFP points to its
+ *       FST.</li>
+ *   <li>DirOffset is a pointer to the start of the IndexStartFPs
+ *       for all fields</li>
+ *   <li>It's possible that an on-disk block would contain
+ *       too many terms (more than the allowed maximum
+ *       (default: 48)).  When this happens, the block is
+ *       sub-divided into new blocks (called "floor
+ *       blocks"), and then the output in the FST for the
+ *       block's prefix encodes the leading byte of each
+ *       sub-block, and its file pointer.
+ * </ul>
+ *
+ * @see EncryptedBlockTreeTermsReader
+ * @lucene.experimental
+ */
+public final class EncryptedBlockTreeTermsWriter extends FieldsConsumer {
+
+  /** Suggested default value for the {@code
+   *  minItemsInBlock} parameter to {@link
+   *  #EncryptedBlockTreeTermsWriter(CipherFactory, SegmentWriteState, PostingsWriterBase, int, int)}. */
+  public final static int DEFAULT_MIN_BLOCK_SIZE = 25;
+
+  /** Suggested default value for the {@code
+   *  maxItemsInBlock} parameter to {@link
+   *  #EncryptedBlockTreeTermsWriter(CipherFactory, SegmentWriteState, PostingsWriterBase, int, int)}. */
+  public final static int DEFAULT_MAX_BLOCK_SIZE = 48;
+
+  //public static boolean DEBUG = false;
+  //public static boolean DEBUG2 = false;
+
+  //private final static boolean SAVE_DOT_FILES = false;
+
+  private final IndexOutput termsOut;
+  private final IndexOutput indexOut;
+  final int maxDoc;
+  final int minItemsInBlock;
+  final int maxItemsInBlock;
+  final int minItemsInAutoPrefix;
+  final int maxItemsInAutoPrefix;
+
+  final Cipher encipher;
+
+  final PostingsWriterBase postingsWriter;
+  final FieldInfos fieldInfos;
+
+  private static class FieldMetaData {
+    public final FieldInfo fieldInfo;
+    public final BytesRef rootCode;
+    public final long numTerms;
+    public final long indexStartFP;
+    public final long sumTotalTermFreq;
+    public final long sumDocFreq;
+    public final int docCount;
+    private final int longsSize;
+    public final BytesRef minTerm;
+    public final BytesRef maxTerm;
+
+    public FieldMetaData(FieldInfo fieldInfo, BytesRef rootCode, long numTerms, long indexStartFP, long sumTotalTermFreq, long sumDocFreq, int docCount, int longsSize,
+                         BytesRef minTerm, BytesRef maxTerm) {
+      assert numTerms > 0;
+      this.fieldInfo = fieldInfo;
+      assert rootCode != null: "field=" + fieldInfo.name + " numTerms=" + numTerms;
+      this.rootCode = rootCode;
+      this.indexStartFP = indexStartFP;
+      this.numTerms = numTerms;
+      this.sumTotalTermFreq = sumTotalTermFreq;
+      this.sumDocFreq = sumDocFreq;
+      this.docCount = docCount;
+      this.longsSize = longsSize;
+      this.minTerm = minTerm;
+      this.maxTerm = maxTerm;
+    }
+  }
+
+  private final List<FieldMetaData> fields = new ArrayList<>();
+
+  // private final String segment;
+  final FixedBitSet prefixDocs;
+
+  /** Reused in getAutoPrefixTermsEnum: */
+  final BitSetTermsEnum prefixFixedBitsTermsEnum;
+
+  /** Reused in getAutoPrefixTermsEnum: */
+  private TermsEnum prefixTermsEnum;
+
+  /** Reused in getAutoPrefixTermsEnum: */
+  private PostingsEnum prefixDocsEnum;
+
+  /** Create a new writer, using default values for auto-prefix terms. */
+  public EncryptedBlockTreeTermsWriter(CipherFactory cipherFactory,
+                                       SegmentWriteState state,
+                                       PostingsWriterBase postingsWriter,
+                                       int minItemsInBlock,
+                                       int maxItemsInBlock) throws IOException {
+    this(cipherFactory, state, postingsWriter, minItemsInBlock, maxItemsInBlock, 0, 0);
+  }
+
+  /** Create a new writer.  The number of items (terms or
+   *  sub-blocks) per block will aim to be between
+   *  minItemsPerBlock and maxItemsPerBlock, though in some
+   *  cases the blocks may be smaller than the min.
+   *  For DOCS_ONLY fields, this terms dictionary will
+   *  insert automatically generated prefix terms for common
+   *  prefixes, as long as each prefix matches at least
+   *  {@code minItemsInAutoPrefix} other terms or prefixes,
+   *  and at most {@code maxItemsInAutoPrefix} other terms
+   *  or prefixes.  Set {@code minItemsInAutoPrefix} to 0
+   *  to disable auto-prefix terms. */
+  public EncryptedBlockTreeTermsWriter(CipherFactory cipherFactory,
+                                       SegmentWriteState state,
+                                       PostingsWriterBase postingsWriter,
+                                       int minItemsInBlock,
+                                       int maxItemsInBlock,
+                                       int minItemsInAutoPrefix,
+                                       int maxItemsInAutoPrefix)
+    throws IOException
+  {
+    // Terms writer is single-threaded. We can instantiate the encipher here.
+    this.encipher = cipherFactory.newEncipherInstance();
+
+    validateSettings(minItemsInBlock,
+                     maxItemsInBlock);
+
+    this.minItemsInBlock = minItemsInBlock;
+    this.maxItemsInBlock = maxItemsInBlock;
+
+    validateAutoPrefixSettings(minItemsInAutoPrefix,
+                               maxItemsInAutoPrefix);
+
+    if (minItemsInAutoPrefix != 0) {
+      // TODO: can we used compressed bitset instead?  that auto-upgrades if it's dense enough...
+      prefixDocs = new FixedBitSet(state.segmentInfo.maxDoc());
+      prefixFixedBitsTermsEnum = new BitSetTermsEnum(prefixDocs);
+    } else {
+      prefixDocs = null;
+      prefixFixedBitsTermsEnum = null;
+    }
+
+    this.minItemsInAutoPrefix = minItemsInAutoPrefix;
+    this.maxItemsInAutoPrefix = maxItemsInAutoPrefix;
+
+    this.maxDoc = state.segmentInfo.maxDoc();
+    this.fieldInfos = state.fieldInfos;
+    this.postingsWriter = postingsWriter;
+
+    final String termsName = IndexFileNames.segmentFileName(state.segmentInfo.name, state.segmentSuffix, EncryptedBlockTreeTermsReader.TERMS_EXTENSION);
+    termsOut = state.directory.createOutput(termsName, state.context);
+    boolean success = false;
+    IndexOutput indexOut = null;
+    try {
+      CodecUtil.writeIndexHeader(termsOut, EncryptedBlockTreeTermsReader.TERMS_CODEC_NAME, EncryptedBlockTreeTermsReader.VERSION_CURRENT,
+                                 state.segmentInfo.getId(), state.segmentSuffix);
+
+      // So at read time we know, globally, that there will be no auto-prefix terms:
+      if (minItemsInAutoPrefix == 0) {
+        termsOut.writeByte((byte) 0);
+      } else {
+        termsOut.writeByte((byte) 1);
+      }
+
+      final String indexName = IndexFileNames.segmentFileName(state.segmentInfo.name, state.segmentSuffix, EncryptedBlockTreeTermsReader.TERMS_INDEX_EXTENSION);
+      indexOut = state.directory.createOutput(indexName, state.context);
+      CodecUtil.writeIndexHeader(indexOut, EncryptedBlockTreeTermsReader.TERMS_INDEX_CODEC_NAME, EncryptedBlockTreeTermsReader.VERSION_CURRENT,
+                                 state.segmentInfo.getId(), state.segmentSuffix);
+      //segment = state.segmentInfo.name;
+
+      postingsWriter.init(termsOut, state);                          // have consumer write its format/header
+      
+      this.indexOut = indexOut;
+      success = true;
+    } finally {
+      if (!success) {
+        IOUtils.closeWhileHandlingException(termsOut, indexOut);
+      }
+    }
+  }
+
+  /** Writes the terms file trailer. */
+  private void writeTrailer(IndexOutput out, long dirStart) throws IOException {
+    out.writeLong(dirStart);    
+  }
+
+  /** Writes the index file trailer. */
+  private void writeIndexTrailer(IndexOutput indexOut, long dirStart) throws IOException {
+    indexOut.writeLong(dirStart);    
+  }
+
+  /** Throws {@code IllegalArgumentException} if any of these settings
+   *  is invalid. */
+  public static void validateSettings(int minItemsInBlock, int maxItemsInBlock) {
+    if (minItemsInBlock <= 1) {
+      throw new IllegalArgumentException("minItemsInBlock must be >= 2; got " + minItemsInBlock);
+    }
+    if (minItemsInBlock > maxItemsInBlock) {
+      throw new IllegalArgumentException("maxItemsInBlock must be >= minItemsInBlock; got maxItemsInBlock=" + maxItemsInBlock + " minItemsInBlock=" + minItemsInBlock);
+    }
+    if (2*(minItemsInBlock-1) > maxItemsInBlock) {
+      throw new IllegalArgumentException("maxItemsInBlock must be at least 2*(minItemsInBlock-1); got maxItemsInBlock=" + maxItemsInBlock + " minItemsInBlock=" + minItemsInBlock);
+    }
+  }
+
+  /** Throws {@code IllegalArgumentException} if any of these settings
+   *  is invalid. */
+  public static void validateAutoPrefixSettings(int minItemsInAutoPrefix,
+                                                int maxItemsInAutoPrefix) {
+    if (minItemsInAutoPrefix != 0) {
+      if (minItemsInAutoPrefix < 2) {
+        throw new IllegalArgumentException("minItemsInAutoPrefix must be at least 2; got minItemsInAutoPrefix=" + minItemsInAutoPrefix);
+      }
+      if (minItemsInAutoPrefix > maxItemsInAutoPrefix) {
+        throw new IllegalArgumentException("maxItemsInAutoPrefix must be >= minItemsInAutoPrefix; got maxItemsInAutoPrefix=" + maxItemsInAutoPrefix + " minItemsInAutoPrefix=" + minItemsInAutoPrefix);
+      }
+      if (2*(minItemsInAutoPrefix-1) > maxItemsInAutoPrefix) {
+        throw new IllegalArgumentException("maxItemsInAutoPrefix must be at least 2*(minItemsInAutoPrefix-1); got maxItemsInAutoPrefix=" + maxItemsInAutoPrefix + " minItemsInAutoPrefix=" + minItemsInAutoPrefix);
+      }
+    } else if (maxItemsInAutoPrefix != 0) {
+      throw new IllegalArgumentException("maxItemsInAutoPrefix must be 0 (disabled) when minItemsInAutoPrefix is 0");
+    }
+  }
+
+  @Override
+  public void write(Fields fields) throws IOException {
+    //if (DEBUG) System.out.println("\nBTTW.write seg=" + segment);
+
+    String lastField = null;
+    for(String field : fields) {
+      assert lastField == null || lastField.compareTo(field) < 0;
+      lastField = field;
+
+      //if (DEBUG) System.out.println("\nBTTW.write seg=" + segment + " field=" + field);
+      Terms terms = fields.terms(field);
+      if (terms == null) {
+        continue;
+      }
+      FieldInfo fieldInfo = fieldInfos.fieldInfo(field);
+
+      // First pass to find all prefix terms we should compile into the index:
+      List<AutoPrefixTermsWriter.PrefixTerm> prefixTerms;
+      if (minItemsInAutoPrefix != 0) {
+        if (fieldInfo.getIndexOptions() != IndexOptions.DOCS) {
+          throw new IllegalStateException("ranges can only be indexed with IndexOptions.DOCS (field: " + fieldInfo.name + ")");
+        }
+        prefixTerms = new AutoPrefixTermsWriter(terms, minItemsInAutoPrefix, maxItemsInAutoPrefix).prefixes;
+        //if (DEBUG) {
+        //  for(PrefixTerm term : prefixTerms) {
+        //    System.out.println("field=" + fieldInfo.name + " PREFIX TERM: " + term);
+        //  }
+        //}
+      } else {
+        prefixTerms = null;
+      }
+
+      TermsEnum termsEnum = terms.iterator();
+      TermsWriter termsWriter = new TermsWriter(fieldInfos.fieldInfo(field));
+      int prefixTermUpto = 0;
+      while (true) {
+        BytesRef term = termsEnum.next();
+        //if (DEBUG) System.out.println("BTTW: next term " + term);
+
+        // Insert (merge sort) next prefix term(s):
+        if (prefixTerms != null) {
+          while (prefixTermUpto < prefixTerms.size() && (term == null || prefixTerms.get(prefixTermUpto).compareTo(term) <= 0)) {
+            AutoPrefixTermsWriter.PrefixTerm prefixTerm = prefixTerms.get(prefixTermUpto);
+            //if (DEBUG) System.out.println("seg=" + segment + " field=" + fieldInfo.name + " NOW INSERT prefix=" + prefixTerm);
+            termsWriter.write(prefixTerm.term, getAutoPrefixTermsEnum(terms, prefixTerm), prefixTerm);
+            prefixTermUpto++;
+          }
+        }
+
+        if (term == null) {
+          break;
+        }
+
+        //if (DEBUG) System.out.println("write field=" + fieldInfo.name + " term=" + brToString(term));
+        termsWriter.write(term, termsEnum, null);
+      }
+
+      assert prefixTerms == null || prefixTermUpto == prefixTerms.size();
+
+      termsWriter.finish();
+
+      //if (DEBUG) System.out.println("\nBTTW.write done seg=" + segment + " field=" + field);
+    }
+  }
+
+  private TermsEnum getAutoPrefixTermsEnum(Terms terms, final AutoPrefixTermsWriter.PrefixTerm prefix) throws IOException {
+    assert prefixDocs != null;
+    prefixDocs.clear(0, prefixDocs.length());
+
+    prefixTermsEnum = prefix.getTermsEnum(terms.iterator());
+
+    //System.out.println("BTTW.getAutoPrefixTE: prefix=" + prefix);
+    while (prefixTermsEnum.next() != null) {
+      //System.out.println("    got term=" + prefixTermsEnum.term().utf8ToString());
+      //termCount++;
+      prefixDocsEnum = prefixTermsEnum.postings(prefixDocsEnum, 0);
+      //System.out.println("      " + prefixDocsEnum + " doc=" + prefixDocsEnum.docID());
+      prefixDocs.or(prefixDocsEnum);
+    }
+
+    //System.out.println("  done terms: " + prefixDocs.cardinality() + " doc seen; " + termCount + " terms seen");
+    return prefixFixedBitsTermsEnum;
+  }
+  
+  static long encodeOutput(long fp, boolean hasTerms, boolean isFloor) {
+    assert fp < (1L << 62);
+    return (fp << 2) | (hasTerms ? EncryptedBlockTreeTermsReader.OUTPUT_FLAG_HAS_TERMS : 0) | (isFloor ? EncryptedBlockTreeTermsReader.OUTPUT_FLAG_IS_FLOOR : 0);
+  }
+
+  private static class PendingEntry {
+    public final boolean isTerm;
+
+    protected PendingEntry(boolean isTerm) {
+      this.isTerm = isTerm;
+    }
+  }
+
+  private static final class PendingTerm extends PendingEntry {
+    public final byte[] termBytes;
+    // stats + metadata
+    public final BlockTermState state;
+    // Non-null if this is an auto-prefix-term:
+    public final AutoPrefixTermsWriter.PrefixTerm prefixTerm;
+    public PendingTerm other;
+
+    public PendingTerm(BytesRef term, BlockTermState state, AutoPrefixTermsWriter.PrefixTerm prefixTerm) {
+      super(true);
+      this.termBytes = new byte[term.length];
+      System.arraycopy(term.bytes, term.offset, termBytes, 0, term.length);
+      this.state = state;
+      this.prefixTerm = prefixTerm;
+    }
+
+    @Override
+    public String toString() {
+      return "TERM: " + brToString(termBytes);
+    }
+  }
+
+  // for debugging
+  @SuppressWarnings("unused")
+  static String brToString(BytesRef b) {
+    if (b == null) {
+      return "(null)";
+    } else {
+      try {
+        return b.utf8ToString() + " " + b;
+      } catch (Throwable t) {
+        // If BytesRef isn't actually UTF8, or it's eg a
+        // prefix of UTF8 that ends mid-unicode-char, we
+        // fallback to hex:
+        return b.toString();
+      }
+    }
+  }
+
+  // for debugging
+  @SuppressWarnings("unused")
+  static String brToString(byte[] b) {
+    return brToString(new BytesRef(b));
+  }
+
+  private static final class PendingBlock extends PendingEntry {
+    public final BytesRef prefix;
+    public final long fp;
+    public FST<BytesRef> index;
+    public List<FST<BytesRef>> subIndices;
+    public final boolean hasTerms;
+    public final boolean isFloor;
+    public final int floorLeadByte;
+
+    public PendingBlock(BytesRef prefix, long fp, boolean hasTerms, boolean isFloor, int floorLeadByte, List<FST<BytesRef>> subIndices) {
+      super(false);
+      this.prefix = prefix;
+      this.fp = fp;
+      this.hasTerms = hasTerms;
+      this.isFloor = isFloor;
+      this.floorLeadByte = floorLeadByte;
+      this.subIndices = subIndices;
+    }
+
+    @Override
+    public String toString() {
+      return "BLOCK: prefix=" + brToString(prefix);
+    }
+
+    public void compileIndex(List<PendingBlock> blocks, RAMOutputStream scratchBytes, IntsRefBuilder scratchIntsRef) throws IOException {
+
+      assert (isFloor && blocks.size() > 1) || (isFloor == false && blocks.size() == 1): "isFloor=" + isFloor + " blocks=" + blocks;
+      assert this == blocks.get(0);
+
+      assert scratchBytes.getFilePointer() == 0;
+
+      // TODO: try writing the leading vLong in MSB order
+      // (opposite of what Lucene does today), for better
+      // outputs sharing in the FST
+      scratchBytes.writeVLong(encodeOutput(fp, hasTerms, isFloor));
+      if (isFloor) {
+        scratchBytes.writeVInt(blocks.size()-1);
+        for (int i=1;i<blocks.size();i++) {
+          PendingBlock sub = blocks.get(i);
+          assert sub.floorLeadByte != -1;
+          //if (DEBUG) {
+          //  System.out.println("    write floorLeadByte=" + Integer.toHexString(sub.floorLeadByte&0xff));
+          //}
+          scratchBytes.writeByte((byte) sub.floorLeadByte);
+          assert sub.fp > fp;
+          scratchBytes.writeVLong((sub.fp - fp) << 1 | (sub.hasTerms ? 1 : 0));
+        }
+      }
+
+      final ByteSequenceOutputs outputs = ByteSequenceOutputs.getSingleton();
+      final Builder<BytesRef> indexBuilder = new Builder<>(FST.INPUT_TYPE.BYTE1,
+                                                           0, 0, true, false, Integer.MAX_VALUE,
+                                                           outputs, false,
+                                                           PackedInts.COMPACT, true, 15);
+      //if (DEBUG) {
+      //  System.out.println("  compile index for prefix=" + prefix);
+      //}
+      //indexBuilder.DEBUG = false;
+      final byte[] bytes = new byte[(int) scratchBytes.getFilePointer()];
+      assert bytes.length > 0;
+      scratchBytes.writeTo(bytes, 0);
+      indexBuilder.add(Util.toIntsRef(prefix, scratchIntsRef), new BytesRef(bytes, 0, bytes.length));
+      scratchBytes.reset();
+
+      // Copy over index for all sub-blocks
+      for(PendingBlock block : blocks) {
+        if (block.subIndices != null) {
+          for(FST<BytesRef> subIndex : block.subIndices) {
+            append(indexBuilder, subIndex, scratchIntsRef);
+          }
+          block.subIndices = null;
+        }
+      }
+
+      index = indexBuilder.finish();
+
+      assert subIndices == null;
+
+      /*
+      Writer w = new OutputStreamWriter(new FileOutputStream("out.dot"));
+      Util.toDot(index, w, false, false);
+      System.out.println("SAVED to out.dot");
+      w.close();
+      */
+    }
+
+    // TODO: maybe we could add bulk-add method to
+    // Builder?  Takes FST and unions it w/ current
+    // FST.
+    private void append(Builder<BytesRef> builder, FST<BytesRef> subIndex, IntsRefBuilder scratchIntsRef) throws IOException {
+      final BytesRefFSTEnum<BytesRef> subIndexEnum = new BytesRefFSTEnum<>(subIndex);
+      BytesRefFSTEnum.InputOutput<BytesRef> indexEnt;
+      while((indexEnt = subIndexEnum.next()) != null) {
+        //if (DEBUG) {
+        //  System.out.println("      add sub=" + indexEnt.input + " " + indexEnt.input + " output=" + indexEnt.output);
+        //}
+        builder.add(Util.toIntsRef(indexEnt.input, scratchIntsRef), indexEnt.output);
+      }
+    }
+  }
+
+  private final RAMOutputStream scratchBytes = new RAMOutputStream();
+  private final IntsRefBuilder scratchIntsRef = new IntsRefBuilder();
+
+  static final BytesRef EMPTY_BYTES_REF = new BytesRef();
+
+  class TermsWriter {
+    private final FieldInfo fieldInfo;
+    private final int longsSize;
+    private long numTerms;
+    final FixedBitSet docsSeen;
+    long sumTotalTermFreq;
+    long sumDocFreq;
+    long indexStartFP;
+
+    // Records index into pending where the current prefix at that
+    // length "started"; for example, if current term starts with 't',
+    // startsByPrefix[0] is the index into pending for the first
+    // term/sub-block starting with 't'.  We use this to figure out when
+    // to write a new block:
+    private final BytesRefBuilder lastTerm = new BytesRefBuilder();
+    private int[] prefixStarts = new int[8];
+
+    private final long[] longs;
+
+    // Pending stack of terms and blocks.  As terms arrive (in sorted order)
+    // we append to this stack, and once the top of the stack has enough
+    // terms starting with a common prefix, we write a new block with
+    // those terms and replace those terms in the stack with a new block:
+    private final List<PendingEntry> pending = new ArrayList<>();
+
+    // Reused in writeBlocks:
+    private final List<PendingBlock> newBlocks = new ArrayList<>();
+
+    private PendingTerm firstPendingTerm;
+    private PendingTerm lastPendingTerm;
+
+    /** Writes the top count entries in pending, using prevTerm to compute the prefix. */
+    void writeBlocks(int prefixLength, int count) throws IOException {
+
+      assert count > 0;
+
+      //if (DEBUG2) {
+      //  BytesRef br = new BytesRef(lastTerm.bytes());
+      //  br.length = prefixLength;
+      //  System.out.println("writeBlocks: seg=" + segment + " prefix=" + brToString(br) + " count=" + count);
+      //}
+
+      // Root block better write all remaining pending entries:
+      assert prefixLength > 0 || count == pending.size();
+
+      int lastSuffixLeadLabel = -1;
+
+      // True if we saw at least one term in this block (we record if a block
+      // only points to sub-blocks in the terms index so we can avoid seeking
+      // to it when we are looking for a term):
+      boolean hasTerms = false;
+      boolean hasPrefixTerms = false;
+      boolean hasSubBlocks = false;
+
+      int start = pending.size()-count;
+      int end = pending.size();
+      int nextBlockStart = start;
+      int nextFloorLeadLabel = -1;
+
+      for (int i=start; i<end; i++) {
+
+        PendingEntry ent = pending.get(i);
+
+        int suffixLeadLabel;
+
+        if (ent.isTerm) {
+          PendingTerm term = (PendingTerm) ent;
+          if (term.termBytes.length == prefixLength) {
+            // Suffix is 0, i.e. prefix 'foo' and term is
+            // 'foo' so the term has empty string suffix
+            // in this block
+            assert lastSuffixLeadLabel == -1: "i=" + i + " lastSuffixLeadLabel=" + lastSuffixLeadLabel;
+            suffixLeadLabel = -1;
+          } else {
+            suffixLeadLabel = term.termBytes[prefixLength] & 0xff;
+          }
+        } else {
+          PendingBlock block = (PendingBlock) ent;
+          assert block.prefix.length > prefixLength;
+          suffixLeadLabel = block.prefix.bytes[block.prefix.offset + prefixLength] & 0xff;
+        }
+        // if (DEBUG) System.out.println("  i=" + i + " ent=" + ent + " suffixLeadLabel=" + suffixLeadLabel);
+
+        if (suffixLeadLabel != lastSuffixLeadLabel) {
+          int itemsInBlock = i - nextBlockStart;
+          if (itemsInBlock >= minItemsInBlock && end-nextBlockStart > maxItemsInBlock) {
+            // The count is too large for one block, so we must break it into "floor" blocks, where we record
+            // the leading label of the suffix of the first term in each floor block, so at search time we can
+            // jump to the right floor block.  We just use a naive greedy segmenter here: make a new floor
+            // block as soon as we have at least minItemsInBlock.  This is not always best: it often produces
+            // a too-small block as the final block:
+            boolean isFloor = itemsInBlock < count;
+            newBlocks.add(writeBlock(prefixLength, isFloor, nextFloorLeadLabel, nextBlockStart, i, hasTerms, hasPrefixTerms, hasSubBlocks));
+
+            hasTerms = false;
+            hasSubBlocks = false;
+            hasPrefixTerms = false;
+            nextFloorLeadLabel = suffixLeadLabel;
+            nextBlockStart = i;
+          }
+
+          lastSuffixLeadLabel = suffixLeadLabel;
+        }
+
+        if (ent.isTerm) {
+          hasTerms = true;
+          hasPrefixTerms |= ((PendingTerm) ent).prefixTerm != null;
+        } else {
+          hasSubBlocks = true;
+        }
+      }
+
+      // Write last block, if any:
+      if (nextBlockStart < end) {
+        int itemsInBlock = end - nextBlockStart;
+        boolean isFloor = itemsInBlock < count;
+        newBlocks.add(writeBlock(prefixLength, isFloor, nextFloorLeadLabel, nextBlockStart, end, hasTerms, hasPrefixTerms, hasSubBlocks));
+      }
+
+      assert newBlocks.isEmpty() == false;
+
+      PendingBlock firstBlock = newBlocks.get(0);
+
+      assert firstBlock.isFloor || newBlocks.size() == 1;
+
+      firstBlock.compileIndex(newBlocks, scratchBytes, scratchIntsRef);
+
+      // Remove slice from the top of the pending stack, that we just wrote:
+      pending.subList(pending.size()-count, pending.size()).clear();
+
+      // Append new block
+      pending.add(firstBlock);
+
+      newBlocks.clear();
+    }
+
+    /** Writes the specified slice (start is inclusive, end is exclusive)
+     *  from pending stack as a new block.  If isFloor is true, there
+     *  were too many (more than maxItemsInBlock) entries sharing the
+     *  same prefix, and so we broke it into multiple floor blocks where
+     *  we record the starting label of the suffix of each floor block. */
+    private PendingBlock writeBlock(int prefixLength, boolean isFloor, int floorLeadLabel, int start, int end,
+                                    boolean hasTerms, boolean hasPrefixTerms, boolean hasSubBlocks) throws IOException {
+
+      assert end > start;
+
+      long startFP = termsOut.getFilePointer();
+
+      boolean hasFloorLeadLabel = isFloor && floorLeadLabel != -1;
+
+      final BytesRef prefix = new BytesRef(prefixLength + (hasFloorLeadLabel ? 1 : 0));
+      System.arraycopy(lastTerm.get().bytes, 0, prefix.bytes, 0, prefixLength);
+      prefix.length = prefixLength;
+
+      //if (DEBUG2) System.out.println("    writeBlock field=" + fieldInfo.name + " prefix=" + brToString(prefix) + " fp=" + startFP + " isFloor=" + isFloor + " isLastInFloor=" + (end == pending.size()) + " floorLeadLabel=" + floorLeadLabel + " start=" + start + " end=" + end + " hasTerms=" + hasTerms + " hasSubBlocks=" + hasSubBlocks);
+
+      // Write block header:
+      int numEntries = end - start;
+      int code = numEntries << 1;
+      if (end == pending.size()) {
+        // Last block:
+        code |= 1;
+      }
+      termsOut.writeVInt(code);
+
+      /*
+      if (DEBUG) {
+        System.out.println("  writeBlock " + (isFloor ? "(floor) " : "") + "seg=" + segment + " pending.size()=" + pending.size() + " prefixLength=" + prefixLength + " indexPrefix=" + brToString(prefix) + " entCount=" + (end-start+1) + " startFP=" + startFP + (isFloor ? (" floorLeadLabel=" + Integer.toHexString(floorLeadLabel)) : ""));
+      }
+      */
+
+      // 1st pass: pack term suffix bytes into byte[] blob
+      // TODO: cutover to bulk int codec... simple64?
+
+      // We optimize the leaf block case (block has only terms), writing a more
+      // compact format in this case:
+      boolean isLeafBlock = hasSubBlocks == false && hasPrefixTerms == false;
+
+      //System.out.println("  isLeaf=" + isLeafBlock);
+
+      final List<FST<BytesRef>> subIndices;
+
+      boolean absolute = true;
+
+      if (isLeafBlock) {
+        // Block contains only ordinary terms:
+        subIndices = null;
+        for (int i=start;i<end;i++) {
+          PendingEntry ent = pending.get(i);
+          assert ent.isTerm: "i=" + i;
+
+          PendingTerm term = (PendingTerm) ent;
+          assert term.prefixTerm == null;
+
+          assert StringHelper.startsWith(term.termBytes, prefix): "term.term=" + term.termBytes + " prefix=" + prefix;
+          BlockTermState state = term.state;
+          final int suffix = term.termBytes.length - prefixLength;
+          //if (DEBUG2) {
+          //  BytesRef suffixBytes = new BytesRef(suffix);
+          //  System.arraycopy(term.termBytes, prefixLength, suffixBytes.bytes, 0, suffix);
+          //  suffixBytes.length = suffix;
+          //  System.out.println("    write term suffix=" + brToString(suffixBytes));
+          //}
+
+          // For leaf block we write suffix straight
+          suffixWriter.writeVInt(suffix);
+          suffixWriter.writeBytes(term.termBytes, prefixLength, suffix);
+          assert floorLeadLabel == -1 || (term.termBytes[prefixLength] & 0xff) >= floorLeadLabel;
+
+          // Write term stats, to separate byte[] blob:
+          statsWriter.writeVInt(state.docFreq);
+          if (fieldInfo.getIndexOptions() != IndexOptions.DOCS) {
+            assert state.totalTermFreq >= state.docFreq: state.totalTermFreq + " vs " + state.docFreq;
+            statsWriter.writeVLong(state.totalTermFreq - state.docFreq);
+          }
+
+          // Write term meta data
+          postingsWriter.encodeTerm(longs, bytesWriter, fieldInfo, state, absolute);
+          for (int pos = 0; pos < longsSize; pos++) {
+            assert longs[pos] >= 0;
+            metaWriter.writeVLong(longs[pos]);
+          }
+          bytesWriter.writeTo(metaWriter);
+          bytesWriter.reset();
+          absolute = false;
+        }
+      } else {
+        // Block has at least one prefix term or a sub block:
+        subIndices = new ArrayList<>();
+        boolean sawAutoPrefixTerm = false;
+        for (int i=start;i<end;i++) {
+          PendingEntry ent = pending.get(i);
+          if (ent.isTerm) {
+            PendingTerm term = (PendingTerm) ent;
+
+            assert StringHelper.startsWith(term.termBytes, prefix): "term.term=" + term.termBytes + " prefix=" + prefix;
+            BlockTermState state = term.state;
+            final int suffix = term.termBytes.length - prefixLength;
+            //if (DEBUG2) {
+            //  BytesRef suffixBytes = new BytesRef(suffix);
+            //  System.arraycopy(term.termBytes, prefixLength, suffixBytes.bytes, 0, suffix);
+            //  suffixBytes.length = suffix;
+            //  System.out.println("      write term suffix=" + brToString(suffixBytes));
+            //  if (term.prefixTerm != null) {
+            //    System.out.println("        ** auto-prefix term: " + term.prefixTerm);
+            //  }
+            //}
+
+            // For non-leaf block we borrow 1 bit to record
+            // if entry is term or sub-block, and 1 bit to record if
+            // it's a prefix term.  Terms cannot be larger than ~32 KB
+            // so we won't run out of bits:
+
+            if (minItemsInAutoPrefix == 0) {
+              suffixWriter.writeVInt(suffix << 1);
+              suffixWriter.writeBytes(term.termBytes, prefixLength, suffix);
+            } else {
+              code = suffix<<2;
+              int floorLeadEnd = -1;
+              if (term.prefixTerm != null) {
+                assert minItemsInAutoPrefix > 0;
+                sawAutoPrefixTerm = true;
+                AutoPrefixTermsWriter.PrefixTerm prefixTerm = term.prefixTerm;
+                floorLeadEnd = prefixTerm.floorLeadEnd;
+                assert floorLeadEnd != -1;
+
+                if (prefixTerm.floorLeadStart == -2) {
+                  // Starts with empty string
+                  code |= 2;
+                } else {
+                  code |= 3;
+                }
+              }
+              suffixWriter.writeVInt(code);
+              suffixWriter.writeBytes(term.termBytes, prefixLength, suffix);
+              if (floorLeadEnd != -1) {
+                suffixWriter.writeByte((byte) floorLeadEnd);
+              }
+              assert floorLeadLabel == -1 || (term.termBytes[prefixLength] & 0xff) >= floorLeadLabel;
+            }
+
+            // Write term stats, to separate byte[] blob:
+            statsWriter.writeVInt(state.docFreq);
+            if (fieldInfo.getIndexOptions() != IndexOptions.DOCS) {
+              assert state.totalTermFreq >= state.docFreq;
+              statsWriter.writeVLong(state.totalTermFreq - state.docFreq);
+            }
+
+            // TODO: now that terms dict "sees" these longs,
+            // we can explore better column-stride encodings
+            // to encode all long[0]s for this block at
+            // once, all long[1]s, etc., e.g. using
+            // Simple64.  Alternatively, we could interleave
+            // stats + meta ... no reason to have them
+            // separate anymore:
+
+            // Write term meta data
+            postingsWriter.encodeTerm(longs, bytesWriter, fieldInfo, state, absolute);
+            for (int pos = 0; pos < longsSize; pos++) {
+              assert longs[pos] >= 0;
+              metaWriter.writeVLong(longs[pos]);
+            }
+            bytesWriter.writeTo(metaWriter);
+            bytesWriter.reset();
+            absolute = false;
+          } else {
+            PendingBlock block = (PendingBlock) ent;
+            assert StringHelper.startsWith(block.prefix, prefix);
+            final int suffix = block.prefix.length - prefixLength;
+            assert StringHelper.startsWith(block.prefix, prefix);
+
+            assert suffix > 0;
+
+            // For non-leaf block we borrow 1 bit to record
+            // if entry is term or sub-block, and 1 bit (unset here) to
+            // record if it's a prefix term:
+            if (minItemsInAutoPrefix == 0) {
+              suffixWriter.writeVInt((suffix<<1)|1);
+            } else {
+              suffixWriter.writeVInt((suffix<<2)|1);
+            }
+            suffixWriter.writeBytes(block.prefix.bytes, prefixLength, suffix);
+
+            //if (DEBUG2) {
+            //  BytesRef suffixBytes = new BytesRef(suffix);
+            //  System.arraycopy(block.prefix.bytes, prefixLength, suffixBytes.bytes, 0, suffix);
+            //  suffixBytes.length = suffix;
+            //  System.out.println("      write sub-block suffix=" + brToString(suffixBytes) + " subFP=" + block.fp + " subCode=" + (startFP-block.fp) + " floor=" + block.isFloor);
+            //}
+
+            assert floorLeadLabel == -1 || (block.prefix.bytes[prefixLength] & 0xff) >= floorLeadLabel: "floorLeadLabel=" + floorLeadLabel + " suffixLead=" + (block.prefix.bytes[prefixLength] & 0xff);
+            assert block.fp < startFP;
+
+            suffixWriter.writeVLong(startFP - block.fp);
+            subIndices.add(block.index);
+          }
+        }
+
+        assert subIndices.size() != 0 || sawAutoPrefixTerm;
+      }
+
+      // TODO: we could block-write the term suffix pointers;
+      // this would take more space but would enable binary
+      // search on lookup
+
+      // Write suffixes byte[] blob to terms dict output:
+      buffer.length = 0; // reset buffer
+      suffixWriter.writeTo(buffer); // write suffixes byte[] to buffer
+      suffixWriter.reset();
+
+      // encrypt suffixes byte[]
+      int size = encipher.getOutputSize(buffer.length);
+      // ensure that the compressedBytesBuffer is large enough to accept encrypted bytes
+      if (size > buffer.bytes.length) {
+        buffer.bytes = ArrayUtil.grow(buffer.bytes, size);
+      }
+      int written = 0;
+      try {
+        // we can reuse the buffer as output since the encrypt method is copy-safe
+        written = encipher.doFinal(buffer.bytes, 0, buffer.length, buffer.bytes, 0);
+      } catch (Exception e) {
+        throw new IOException("Enciphering of term suffix block failed", e);
+      }
+
+      // Write encrypted suffixes byte[] with its size
+      termsOut.writeVInt((written << 1) | (isLeafBlock ? 1:0));
+      termsOut.writeBytes(buffer.bytes, written);
+
+      // Write term stats byte[] blob
+      termsOut.writeVInt((int) statsWriter.getFilePointer());
+      statsWriter.writeTo(termsOut);
+      statsWriter.reset();
+
+      // Write term meta data byte[] blob
+      termsOut.writeVInt((int) metaWriter.getFilePointer());
+      metaWriter.writeTo(termsOut);
+      metaWriter.reset();
+
+      // if (DEBUG) {
+      //   System.out.println("      fpEnd=" + out.getFilePointer());
+      // }
+
+      if (hasFloorLeadLabel) {
+        // We already allocated to length+1 above:
+        prefix.bytes[prefix.length++] = (byte) floorLeadLabel;
+      }
+
+      return new PendingBlock(prefix, startFP, hasTerms, isFloor, floorLeadLabel, subIndices);
+    }
+
+    TermsWriter(FieldInfo fieldInfo) {
+      this.fieldInfo = fieldInfo;
+      assert fieldInfo.getIndexOptions() != IndexOptions.NONE;
+      docsSeen = new FixedBitSet(maxDoc);
+
+      this.longsSize = postingsWriter.setField(fieldInfo);
+      this.longs = new long[longsSize];
+    }
+    
+    /** Writes one term's worth of postings. */
+    public void write(BytesRef text, TermsEnum termsEnum, AutoPrefixTermsWriter.PrefixTerm prefixTerm) throws IOException {
+      /*
+      if (DEBUG) {
+        int[] tmp = new int[lastTerm.length];
+        System.arraycopy(prefixStarts, 0, tmp, 0, tmp.length);
+        System.out.println("BTTW: write term=" + brToString(text) + " prefixStarts=" + Arrays.toString(tmp) + " pending.size()=" + pending.size());
+      }
+      */
+
+      BlockTermState state = postingsWriter.writeTerm(text, termsEnum, docsSeen);
+      if (state != null) {
+
+        assert state.docFreq != 0;
+        assert fieldInfo.getIndexOptions() == IndexOptions.DOCS || state.totalTermFreq >= state.docFreq: "postingsWriter=" + postingsWriter;
+        pushTerm(text);
+       
+        PendingTerm term = new PendingTerm(text, state, prefixTerm);
+        pending.add(term);
+        //if (DEBUG) System.out.println("    add pending term = " + text + " pending.size()=" + pending.size());
+
+        if (prefixTerm == null) {
+          // Only increment stats for real terms:
+          sumDocFreq += state.docFreq;
+          sumTotalTermFreq += state.totalTermFreq;
+          numTerms++;
+          if (firstPendingTerm == null) {
+            firstPendingTerm = term;
+          }
+          lastPendingTerm = term;
+        }
+      }
+    }
+
+    /** Pushes the new term to the top of the stack, and writes new blocks. */
+    private void pushTerm(BytesRef text) throws IOException {
+      int limit = Math.min(lastTerm.length(), text.length);
+
+      // Find common prefix between last term and current term:
+      int pos = 0;
+      while (pos < limit && lastTerm.byteAt(pos) == text.bytes[text.offset+pos]) {
+        pos++;
+      }
+
+      // if (DEBUG) System.out.println("  shared=" + pos + "  lastTerm.length=" + lastTerm.length);
+
+      // Close the "abandoned" suffix now:
+      for(int i=lastTerm.length()-1;i>=pos;i--) {
+
+        // How many items on top of the stack share the current suffix
+        // we are closing:
+        int prefixTopSize = pending.size() - prefixStarts[i];
+        if (prefixTopSize >= minItemsInBlock) {
+          // if (DEBUG) System.out.println("pushTerm i=" + i + " prefixTopSize=" + prefixTopSize + " minItemsInBlock=" + minItemsInBlock);
+          writeBlocks(i+1, prefixTopSize);
+          prefixStarts[i] -= prefixTopSize-1;
+        }
+      }
+
+      if (prefixStarts.length < text.length) {
+        prefixStarts = ArrayUtil.grow(prefixStarts, text.length);
+      }
+
+      // Init new tail:
+      for(int i=pos;i<text.length;i++) {
+        prefixStarts[i] = pending.size();
+      }
+
+      lastTerm.copyBytes(text);
+    }
+
+    // Finishes all terms in this field
+    public void finish() throws IOException {
+      if (numTerms > 0) {
+        // if (DEBUG) System.out.println("BTTW: finish prefixStarts=" + Arrays.toString(prefixStarts));
+
+        // Add empty term to force closing of all final blocks:
+        pushTerm(new BytesRef());
+
+        // TODO: if pending.size() is already 1 with a non-zero prefix length
+        // we can save writing a "degenerate" root block, but we have to
+        // fix all the places that assume the root block's prefix is the empty string:
+        pushTerm(new BytesRef());
+        writeBlocks(0, pending.size());
+
+        // We better have one final "root" block:
+        assert pending.size() == 1 && !pending.get(0).isTerm: "pending.size()=" + pending.size() + " pending=" + pending;
+        final PendingBlock root = (PendingBlock) pending.get(0);
+        assert root.prefix.length == 0;
+        assert root.index.getEmptyOutput() != null;
+
+        // Write FST to index
+        indexStartFP = indexOut.getFilePointer();
+        // encrypt FST
+        buffer.length = 0; // reset buffer
+        root.index.save(buffer);
+        int size = encipher.getOutputSize(buffer.length);
+        // ensure that the compressedBytesBuffer is large enough to accept encrypted bytes
+        if (size > buffer.bytes.length) {
+          buffer.bytes = ArrayUtil.grow(buffer.bytes, size);
+        }
+        int written = 0;
+        try {
+          // we can reuse the buffer as output since the encrypt method is copy-safe
+          written = encipher.doFinal(buffer.bytes, 0, buffer.length, buffer.bytes, 0);
+        } catch (Exception e) {
+          throw new IOException("Enciphering of FST failed", e);
+        }
+        // Write encrypted FST with its size
+        indexOut.writeVInt(written);
+        indexOut.writeBytes(buffer.bytes, written);
+        //System.out.println("  write FST " + indexStartFP + " field=" + fieldInfo.name);
+
+        /*
+        if (DEBUG) {
+          final String dotFileName = segment + "_" + fieldInfo.name + ".dot";
+          Writer w = new OutputStreamWriter(new FileOutputStream(dotFileName));
+          Util.toDot(root.index, w, false, false);
+          System.out.println("SAVED to " + dotFileName);
+          w.close();
+        }
+        */
+        assert firstPendingTerm != null;
+        BytesRef minTerm = new BytesRef(firstPendingTerm.termBytes);
+
+        assert lastPendingTerm != null;
+        BytesRef maxTerm = new BytesRef(lastPendingTerm.termBytes);
+
+        fields.add(new FieldMetaData(fieldInfo,
+                                     ((PendingBlock) pending.get(0)).index.getEmptyOutput(),
+                                     numTerms,
+                                     indexStartFP,
+                                     sumTotalTermFreq,
+                                     sumDocFreq,
+                                     docsSeen.cardinality(),
+                                     longsSize,
+                                     minTerm, maxTerm));
+      } else {
+        assert sumTotalTermFreq == 0 || fieldInfo.getIndexOptions() == IndexOptions.DOCS && sumTotalTermFreq == -1;
+        assert sumDocFreq == 0;
+        assert docsSeen.cardinality() == 0;
+      }
+    }
+
+    /** Reusable buffer for suffixers byte[] encryption */
+    private GrowableByteArrayDataOutput buffer = new GrowableByteArrayDataOutput(512);
+
+    private final RAMOutputStream suffixWriter = new RAMOutputStream();
+    private final RAMOutputStream statsWriter = new RAMOutputStream();
+    private final RAMOutputStream metaWriter = new RAMOutputStream();
+    private final RAMOutputStream bytesWriter = new RAMOutputStream();
+  }
+
+  private boolean closed;
+  
+  @Override
+  public void close() throws IOException {
+    if (closed) {
+      return;
+    }
+    closed = true;
+    
+    boolean success = false;
+    try {
+      
+      final long dirStart = termsOut.getFilePointer();
+      final long indexDirStart = indexOut.getFilePointer();
+
+      termsOut.writeVInt(fields.size());
+      
+      for(FieldMetaData field : fields) {
+        //System.out.println("  field " + field.fieldInfo.name + " " + field.numTerms + " terms");
+        termsOut.writeVInt(field.fieldInfo.number);
+        assert field.numTerms > 0;
+        termsOut.writeVLong(field.numTerms);
+        termsOut.writeVInt(field.rootCode.length);
+        termsOut.writeBytes(field.rootCode.bytes, field.rootCode.offset, field.rootCode.length);
+        assert field.fieldInfo.getIndexOptions() != IndexOptions.NONE;
+        if (field.fieldInfo.getIndexOptions() != IndexOptions.DOCS) {
+          termsOut.writeVLong(field.sumTotalTermFreq);
+        }
+        termsOut.writeVLong(field.sumDocFreq);
+        termsOut.writeVInt(field.docCount);
+        termsOut.writeVInt(field.longsSize);
+        indexOut.writeVLong(field.indexStartFP);
+        writeBytesRef(termsOut, field.minTerm);
+        writeBytesRef(termsOut, field.maxTerm);
+      }
+      writeTrailer(termsOut, dirStart);
+      CodecUtil.writeFooter(termsOut);
+      writeIndexTrailer(indexOut, indexDirStart);
+      CodecUtil.writeFooter(indexOut);
+      success = true;
+    } finally {
+      if (success) {
+        IOUtils.close(termsOut, indexOut, postingsWriter);
+      } else {
+        IOUtils.closeWhileHandlingException(termsOut, indexOut, postingsWriter);
+      }
+    }
+  }
+
+  private static void writeBytesRef(IndexOutput out, BytesRef bytes) throws IOException {
+    out.writeVInt(bytes.length);
+    out.writeBytes(bytes.bytes, bytes.offset, bytes.length);
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedFieldReader.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedFieldReader.java
new file mode 100644
index 0000000..3fccfab
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedFieldReader.java
@@ -0,0 +1,226 @@
+package org.apache.lucene.codecs.encrypted.blocktree;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.index.FieldInfo;
+import org.apache.lucene.index.IndexOptions;
+import org.apache.lucene.index.Terms;
+import org.apache.lucene.index.TermsEnum;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.Accountables;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.RamUsageEstimator;
+import org.apache.lucene.util.automaton.CompiledAutomaton;
+import org.apache.lucene.util.fst.ByteSequenceOutputs;
+import org.apache.lucene.util.fst.FST;
+
+import javax.crypto.Cipher;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * BlockTree's implementation of {@link Terms}.
+ * @lucene.internal
+ */
+public final class EncryptedFieldReader extends Terms implements Accountable {
+
+  // private final boolean DEBUG = BlockTreeTermsWriter.DEBUG;
+
+  private static final long BASE_RAM_BYTES_USED =
+      RamUsageEstimator.shallowSizeOfInstance(EncryptedFieldReader.class)
+      + 3 * RamUsageEstimator.shallowSizeOfInstance(BytesRef.class);
+
+  final long numTerms;
+  final FieldInfo fieldInfo;
+  final long sumTotalTermFreq;
+  final long sumDocFreq;
+  final int docCount;
+  final long indexStartFP;
+  final long rootBlockFP;
+  final BytesRef rootCode;
+  final BytesRef minTerm;
+  final BytesRef maxTerm;
+  final int longsSize;
+  final EncryptedBlockTreeTermsReader parent;
+
+  final FST<BytesRef> index;
+  //private boolean DEBUG;
+
+  EncryptedFieldReader(EncryptedBlockTreeTermsReader parent, FieldInfo fieldInfo, long numTerms, BytesRef rootCode,
+                       long sumTotalTermFreq, long sumDocFreq, int docCount, long indexStartFP, int longsSize,
+                       IndexInput indexIn, BytesRef minTerm, BytesRef maxTerm) throws IOException {
+    assert numTerms > 0;
+    this.fieldInfo = fieldInfo;
+    //DEBUG = BlockTreeTermsReader.DEBUG && fieldInfo.name.equals("id");
+    this.parent = parent;
+    this.numTerms = numTerms;
+    this.sumTotalTermFreq = sumTotalTermFreq; 
+    this.sumDocFreq = sumDocFreq; 
+    this.docCount = docCount;
+    this.indexStartFP = indexStartFP;
+    this.rootCode = rootCode;
+    this.longsSize = longsSize;
+    this.minTerm = minTerm;
+    this.maxTerm = maxTerm;
+
+    // if (DEBUG) {
+    //   System.out.println("BTTR: seg=" + segment + " field=" + fieldInfo.name + " rootBlockCode=" + rootCode + " divisor=" + indexDivisor);
+    // }
+
+    rootBlockFP = (new ByteArrayDataInput(rootCode.bytes, rootCode.offset, rootCode.length)).readVLong() >>> EncryptedBlockTreeTermsReader.OUTPUT_FLAGS_NUM_BITS;
+
+    if (indexIn != null) {
+      final IndexInput clone = indexIn.clone();
+      //System.out.println("start=" + indexStartFP + " field=" + fieldInfo.name);
+
+      // Decrypt FST
+      clone.seek(indexStartFP);
+      int numBytes = clone.readVInt();
+      byte[] encryptedBytesArray = new byte[numBytes];
+      clone.readBytes(encryptedBytesArray, 0, numBytes); // read encrypted FST byte[]
+      int read = 0;
+      try {
+        // decipher instance must be local to ensure thread safety
+        Cipher decipher = parent.cipherFactory.newDecipherInstance(parent.keyVersion);
+        // we can reuse encryptedBytesArray as output since the decrypt method is copy-safe
+        read = decipher.doFinal(encryptedBytesArray, 0, numBytes, encryptedBytesArray, 0);
+      } catch (Exception e) {
+        throw new IOException("Deciphering of FST failed", e);
+      }
+      ByteArrayDataInput fstInput = new ByteArrayDataInput(encryptedBytesArray, 0, read);
+
+      // Load FST
+      index = new FST<>(fstInput, ByteSequenceOutputs.getSingleton());
+        
+      /*
+        if (false) {
+        final String dotFileName = segment + "_" + fieldInfo.name + ".dot";
+        Writer w = new OutputStreamWriter(new FileOutputStream(dotFileName));
+        Util.toDot(index, w, false, false);
+        System.out.println("FST INDEX: SAVED to " + dotFileName);
+        w.close();
+        }
+      */
+    } else {
+      index = null;
+    }
+  }
+
+  @Override
+  public BytesRef getMin() throws IOException {
+    if (minTerm == null) {
+      // Older index that didn't store min/maxTerm
+      return super.getMin();
+    } else {
+      return minTerm;
+    }
+  }
+
+  @Override
+  public BytesRef getMax() throws IOException {
+    if (maxTerm == null) {
+      // Older index that didn't store min/maxTerm
+      return super.getMax();
+    } else {
+      return maxTerm;
+    }
+  }
+
+  /** For debugging -- used by CheckIndex too*/
+  @Override
+  public Stats getStats() throws IOException {
+    // TODO: add auto-prefix terms into stats
+    return new EncryptedSegmentTermsEnum(this).computeBlockStats();
+  }
+
+  @Override
+  public boolean hasFreqs() {
+    return fieldInfo.getIndexOptions().compareTo(IndexOptions.DOCS_AND_FREQS) >= 0;
+  }
+
+  @Override
+  public boolean hasOffsets() {
+    return fieldInfo.getIndexOptions().compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS) >= 0;
+  }
+
+  @Override
+  public boolean hasPositions() {
+    return fieldInfo.getIndexOptions().compareTo(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS) >= 0;
+  }
+    
+  @Override
+  public boolean hasPayloads() {
+    return fieldInfo.hasPayloads();
+  }
+
+  @Override
+  public TermsEnum iterator() throws IOException {
+    return new EncryptedSegmentTermsEnum(this);
+  }
+
+  @Override
+  public long size() {
+    return numTerms;
+  }
+
+  @Override
+  public long getSumTotalTermFreq() {
+    return sumTotalTermFreq;
+  }
+
+  @Override
+  public long getSumDocFreq() {
+    return sumDocFreq;
+  }
+
+  @Override
+  public int getDocCount() {
+    return docCount;
+  }
+
+  @Override
+  public TermsEnum intersect(CompiledAutomaton compiled, BytesRef startTerm) throws IOException {
+    // if (DEBUG) System.out.println("  FieldReader.intersect startTerm=" + BlockTreeTermsWriter.brToString(startTerm));
+    //System.out.println("intersect: " + compiled.type + " a=" + compiled.automaton);
+    // TODO: we could push "it's a range" or "it's a prefix" down into EncryptedIntersectTermsEnum?
+    // can we optimize knowing that...?
+    return new EncryptedIntersectTermsEnum(this, compiled.automaton, compiled.runAutomaton, compiled.commonSuffixRef, startTerm, compiled.sinkState);
+  }
+    
+  @Override
+  public long ramBytesUsed() {
+    return BASE_RAM_BYTES_USED + ((index!=null)? index.ramBytesUsed() : 0);
+  }
+
+  @Override
+  public Collection<Accountable> getChildResources() {
+    if (index == null) {
+      return Collections.emptyList();
+    } else {
+      return Collections.singleton(Accountables.namedAccountable("term index", index));
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "BlockTreeTerms(terms=" + numTerms + ",postings=" + sumDocFreq + ",positions=" + sumTotalTermFreq + ",docs=" + docCount + ")";
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedIntersectTermsEnum.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedIntersectTermsEnum.java
new file mode 100644
index 0000000..c85453e
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedIntersectTermsEnum.java
@@ -0,0 +1,781 @@
+package org.apache.lucene.codecs.encrypted.blocktree;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.index.PostingsEnum;
+import org.apache.lucene.index.TermState;
+import org.apache.lucene.index.Terms;
+import org.apache.lucene.index.TermsEnum;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.RamUsageEstimator;
+import org.apache.lucene.util.StringHelper;
+import org.apache.lucene.util.automaton.Automaton;
+import org.apache.lucene.util.automaton.RunAutomaton;
+import org.apache.lucene.util.automaton.Transition;
+import org.apache.lucene.util.fst.ByteSequenceOutputs;
+import org.apache.lucene.util.fst.FST;
+import org.apache.lucene.util.fst.Outputs;
+
+import javax.crypto.Cipher;
+import java.io.IOException;
+
+/** This is used to implement efficient {@link Terms#intersect} for
+ *  block-tree.  Note that it cannot seek, except for the initial term on
+ *  init.  It just "nexts" through the intersection of the automaton and
+ *  the terms.  It does not use the terms index at all: on init, it
+ *  loads the root block, and scans its way to the initial term.
+ *  Likewise, in next it scans until it finds a term that matches the
+ *  current automaton transition.  If the index has auto-prefix terms
+ *  (only for DOCS_ONLY fields currently) it will visit these terms
+ *  when possible and then skip the real terms that auto-prefix term
+ *  matched. */
+
+final class EncryptedIntersectTermsEnum extends TermsEnum {
+
+  //static boolean DEBUG = BlockTreeTermsWriter.DEBUG;
+
+  final IndexInput in;
+  final static Outputs<BytesRef> fstOutputs = ByteSequenceOutputs.getSingleton();
+
+  EncryptedIntersectTermsEnumFrame[] stack;
+
+  @SuppressWarnings({"rawtypes","unchecked"}) private FST.Arc<BytesRef>[] arcs = new FST.Arc[5];
+
+  final RunAutomaton runAutomaton;
+  final Automaton automaton;
+  final BytesRef commonSuffix;
+
+  private EncryptedIntersectTermsEnumFrame currentFrame;
+  private Transition currentTransition;
+
+  private final BytesRef term = new BytesRef();
+
+  private final FST.BytesReader fstReader;
+
+  private final boolean allowAutoPrefixTerms;
+
+  final EncryptedFieldReader fr;
+
+  // decipher instance must be local to a TermsEnum to ensure thread safety
+  final Cipher decipher;
+
+  /** Which state in the automaton accepts all possible suffixes. */
+  private final int sinkState;
+
+  private BytesRef savedStartTerm;
+
+  /** True if we did return the current auto-prefix term */
+  private boolean useAutoPrefixTerm;
+
+  // TODO: in some cases we can filter by length?  eg
+  // regexp foo*bar must be at least length 6 bytes
+  public EncryptedIntersectTermsEnum(EncryptedFieldReader fr, Automaton automaton, RunAutomaton runAutomaton, BytesRef commonSuffix, BytesRef startTerm, int sinkState) throws IOException {
+    this.fr = fr;
+    this.decipher = fr.parent.cipherFactory.newDecipherInstance(fr.parent.keyVersion);
+    this.sinkState = sinkState;
+
+    assert automaton != null;
+    assert runAutomaton != null;
+
+    this.runAutomaton = runAutomaton;
+    this.allowAutoPrefixTerms = sinkState != -1;
+    this.automaton = automaton;
+    this.commonSuffix = commonSuffix;
+
+    in = fr.parent.termsIn.clone();
+    stack = new EncryptedIntersectTermsEnumFrame[5];
+    for(int idx=0;idx<stack.length;idx++) {
+      stack[idx] = new EncryptedIntersectTermsEnumFrame(this, idx);
+    }
+    for(int arcIdx=0;arcIdx<arcs.length;arcIdx++) {
+      arcs[arcIdx] = new FST.Arc<>();
+    }
+
+    if (fr.index == null) {
+      fstReader = null;
+    } else {
+      fstReader = fr.index.getBytesReader();
+    }
+
+    // TODO: if the automaton is "smallish" we really
+    // should use the terms index to seek at least to
+    // the initial term and likely to subsequent terms
+    // (or, maybe just fallback to ATE for such cases).
+    // Else the seek cost of loading the frames will be
+    // too costly.
+
+    final FST.Arc<BytesRef> arc = fr.index.getFirstArc(arcs[0]);
+    // Empty string prefix must have an output in the index!
+    assert arc.isFinal();
+
+    // Special pushFrame since it's the first one:
+    final EncryptedIntersectTermsEnumFrame f = stack[0];
+    f.fp = f.fpOrig = fr.rootBlockFP;
+    f.prefix = 0;
+    f.setState(runAutomaton.getInitialState());
+    f.arc = arc;
+    f.outputPrefix = arc.output;
+    f.load(fr.rootCode);
+
+    // for assert:
+    assert setSavedStartTerm(startTerm);
+
+    currentFrame = f;
+    if (startTerm != null) {
+      seekToStartTerm(startTerm);
+    }
+    currentTransition = currentFrame.transition;
+  }
+
+  // only for assert:
+  private boolean setSavedStartTerm(BytesRef startTerm) {
+    savedStartTerm = startTerm == null ? null : BytesRef.deepCopyOf(startTerm);
+    return true;
+  }
+
+  @Override
+  public TermState termState() throws IOException {
+    currentFrame.decodeMetaData();
+    return currentFrame.termState.clone();
+  }
+
+  private EncryptedIntersectTermsEnumFrame getFrame(int ord) throws IOException {
+    if (ord >= stack.length) {
+      final EncryptedIntersectTermsEnumFrame[] next = new EncryptedIntersectTermsEnumFrame[ArrayUtil.oversize(1+ord, RamUsageEstimator.NUM_BYTES_OBJECT_REF)];
+      System.arraycopy(stack, 0, next, 0, stack.length);
+      for(int stackOrd=stack.length;stackOrd<next.length;stackOrd++) {
+        next[stackOrd] = new EncryptedIntersectTermsEnumFrame(this, stackOrd);
+      }
+      stack = next;
+    }
+    assert stack[ord].ord == ord;
+    return stack[ord];
+  }
+
+  private FST.Arc<BytesRef> getArc(int ord) {
+    if (ord >= arcs.length) {
+      @SuppressWarnings({"rawtypes","unchecked"}) final FST.Arc<BytesRef>[] next =
+      new FST.Arc[ArrayUtil.oversize(1+ord, RamUsageEstimator.NUM_BYTES_OBJECT_REF)];
+      System.arraycopy(arcs, 0, next, 0, arcs.length);
+      for(int arcOrd=arcs.length;arcOrd<next.length;arcOrd++) {
+        next[arcOrd] = new FST.Arc<>();
+      }
+      arcs = next;
+    }
+    return arcs[ord];
+  }
+
+  private EncryptedIntersectTermsEnumFrame pushFrame(int state) throws IOException {
+    assert currentFrame != null;
+
+    final EncryptedIntersectTermsEnumFrame f = getFrame(currentFrame == null ? 0 : 1+currentFrame.ord);
+        
+    f.fp = f.fpOrig = currentFrame.lastSubFP;
+    f.prefix = currentFrame.prefix + currentFrame.suffix;
+    f.setState(state);
+
+    // Walk the arc through the index -- we only
+    // "bother" with this so we can get the floor data
+    // from the index and skip floor blocks when
+    // possible:
+    FST.Arc<BytesRef> arc = currentFrame.arc;
+    int idx = currentFrame.prefix;
+    assert currentFrame.suffix > 0;
+    BytesRef output = currentFrame.outputPrefix;
+    while (idx < f.prefix) {
+      final int target = term.bytes[idx] & 0xff;
+      // TODO: we could be more efficient for the next()
+      // case by using current arc as starting point,
+      // passed to findTargetArc
+      arc = fr.index.findTargetArc(target, arc, getArc(1+idx), fstReader);
+      assert arc != null;
+      output = fstOutputs.add(output, arc.output);
+      idx++;
+    }
+
+    f.arc = arc;
+    f.outputPrefix = output;
+    assert arc.isFinal();
+    f.load(fstOutputs.add(output, arc.nextFinalOutput));
+    return f;
+  }
+
+  @Override
+  public BytesRef term() {
+    return term;
+  }
+
+  @Override
+  public int docFreq() throws IOException {
+    currentFrame.decodeMetaData();
+    return currentFrame.termState.docFreq;
+  }
+
+  @Override
+  public long totalTermFreq() throws IOException {
+    currentFrame.decodeMetaData();
+    return currentFrame.termState.totalTermFreq;
+  }
+
+  @Override
+  public PostingsEnum postings(PostingsEnum reuse, int flags) throws IOException {
+    currentFrame.decodeMetaData();
+    return fr.parent.postingsReader.postings(fr.fieldInfo, currentFrame.termState, reuse, flags);
+  }
+
+  private int getState() {
+    int state = currentFrame.state;
+    for(int idx=0;idx<currentFrame.suffix;idx++) {
+      state = runAutomaton.step(state,  currentFrame.suffixBytes[currentFrame.startBytePos+idx] & 0xff);
+      assert state != -1;
+    }
+    return state;
+  }
+
+  // NOTE: specialized to only doing the first-time
+  // seek, but we could generalize it to allow
+  // arbitrary seekExact/Ceil.  Note that this is a
+  // seekFloor!
+  private void seekToStartTerm(BytesRef target) throws IOException {
+    assert currentFrame.ord == 0;
+    if (term.length < target.length) {
+      term.bytes = ArrayUtil.grow(term.bytes, target.length);
+    }
+    FST.Arc<BytesRef> arc = arcs[0];
+    assert arc == currentFrame.arc;
+
+    for(int idx=0;idx<=target.length;idx++) {
+
+      while (true) {
+        final int savNextEnt = currentFrame.nextEnt;
+        final int savePos = currentFrame.suffixesReader.getPosition();
+        final int saveStartBytePos = currentFrame.startBytePos;
+        final int saveSuffix = currentFrame.suffix;
+        final long saveLastSubFP = currentFrame.lastSubFP;
+        final int saveTermBlockOrd = currentFrame.termState.termBlockOrd;
+        final boolean saveIsAutoPrefixTerm = currentFrame.isAutoPrefixTerm;
+
+        final boolean isSubBlock = currentFrame.next();
+
+        term.length = currentFrame.prefix + currentFrame.suffix;
+        if (term.bytes.length < term.length) {
+          term.bytes = ArrayUtil.grow(term.bytes, term.length);
+        }
+        System.arraycopy(currentFrame.suffixBytes, currentFrame.startBytePos, term.bytes, currentFrame.prefix, currentFrame.suffix);
+
+        if (isSubBlock && StringHelper.startsWith(target, term)) {
+          // Recurse
+          currentFrame = pushFrame(getState());
+          break;
+        } else {
+          final int cmp = term.compareTo(target);
+          if (cmp < 0) {
+            if (currentFrame.nextEnt == currentFrame.entCount) {
+              if (!currentFrame.isLastInFloor) {
+                // Advance to next floor block
+                currentFrame.loadNextFloorBlock();
+                continue;
+              } else {
+                return;
+              }
+            }
+            continue;
+          } else if (cmp == 0) {
+            if (allowAutoPrefixTerms == false && currentFrame.isAutoPrefixTerm) {
+              continue;
+            }
+            return;
+          } else if (allowAutoPrefixTerms || currentFrame.isAutoPrefixTerm == false) {
+            // Fallback to prior entry: the semantics of
+            // this method is that the first call to
+            // next() will return the term after the
+            // requested term
+            currentFrame.nextEnt = savNextEnt;
+            currentFrame.lastSubFP = saveLastSubFP;
+            currentFrame.startBytePos = saveStartBytePos;
+            currentFrame.suffix = saveSuffix;
+            currentFrame.suffixesReader.setPosition(savePos);
+            currentFrame.termState.termBlockOrd = saveTermBlockOrd;
+            currentFrame.isAutoPrefixTerm = saveIsAutoPrefixTerm;
+            System.arraycopy(currentFrame.suffixBytes, currentFrame.startBytePos, term.bytes, currentFrame.prefix, currentFrame.suffix);
+            term.length = currentFrame.prefix + currentFrame.suffix;
+            // If the last entry was a block we don't
+            // need to bother recursing and pushing to
+            // the last term under it because the first
+            // next() will simply skip the frame anyway
+            return;
+          }
+        }
+      }
+    }
+
+    assert false;
+  }
+
+  private boolean popPushNext() throws IOException {
+    // Pop finished frames
+    while (currentFrame.nextEnt == currentFrame.entCount) {
+      if (!currentFrame.isLastInFloor) {
+        // Advance to next floor block
+        currentFrame.loadNextFloorBlock();
+        break;
+      } else {
+        if (currentFrame.ord == 0) {
+          throw NoMoreTermsException.INSTANCE;
+        }
+        final long lastFP = currentFrame.fpOrig;
+        currentFrame = stack[currentFrame.ord-1];
+        currentTransition = currentFrame.transition;
+        assert currentFrame.lastSubFP == lastFP;
+      }
+    }
+
+    return currentFrame.next();
+  }
+
+  private boolean skipPastLastAutoPrefixTerm() throws IOException {
+    assert currentFrame.isAutoPrefixTerm;
+    useAutoPrefixTerm = false;
+    currentFrame.termState.isRealTerm = true;
+
+    // If we last returned an auto-prefix term, we must now skip all
+    // actual terms sharing that prefix.  At most, that skipping
+    // requires popping one frame, but it can also require simply
+    // scanning ahead within the current frame.  This scanning will
+    // skip sub-blocks that contain many terms, which is why the
+    // optimization "works":
+    int floorSuffixLeadEnd = currentFrame.floorSuffixLeadEnd;
+
+    boolean isSubBlock;
+
+    if (floorSuffixLeadEnd == -1) {
+      // An ordinary prefix, e.g. foo*
+      int prefix = currentFrame.prefix;
+      int suffix = currentFrame.suffix;
+      if (suffix == 0) {
+
+        // Easy case: the prefix term's suffix is the empty string,
+        // meaning the prefix corresponds to all terms in the
+        // current block, so we just pop this entire block:
+        if (currentFrame.ord == 0) {
+          throw NoMoreTermsException.INSTANCE;
+        }
+        currentFrame = stack[currentFrame.ord-1];
+        currentTransition = currentFrame.transition;
+
+        return popPushNext();
+
+      } else {
+
+        // Just next() until we hit an entry that doesn't share this
+        // prefix.  The first next should be a sub-block sharing the
+        // same prefix, because if there are enough terms matching a
+        // given prefix to warrant an auto-prefix term, then there
+        // must also be enough to make a sub-block (assuming
+        // minItemsInPrefix > minItemsInBlock):
+        scanPrefix:
+        while (true) {
+          if (currentFrame.nextEnt == currentFrame.entCount) {
+            if (currentFrame.isLastInFloor == false) {
+              currentFrame.loadNextFloorBlock();
+            } else if (currentFrame.ord == 0) {
+              throw NoMoreTermsException.INSTANCE;
+            } else {
+              // Pop frame, which also means we've moved beyond this
+              // auto-prefix term:
+              currentFrame = stack[currentFrame.ord-1];
+              currentTransition = currentFrame.transition;
+
+              return popPushNext();
+            }
+          }
+          isSubBlock = currentFrame.next();
+          for(int i=0;i<suffix;i++) {
+            if (term.bytes[prefix+i] != currentFrame.suffixBytes[currentFrame.startBytePos+i]) {
+              break scanPrefix;
+            }
+          }
+        }
+      }
+    } else {
+      // Floor'd auto-prefix term; in this case we must skip all
+      // terms e.g. matching foo[a-m]*.  We are currently "on" fooa,
+      // which the automaton accepted (fooa* through foom*), and
+      // floorSuffixLeadEnd is m, so we must now scan to foon:
+      int prefix = currentFrame.prefix;
+      int suffix = currentFrame.suffix;
+
+      if (currentFrame.floorSuffixLeadStart == -1) {
+        suffix++;
+      }
+
+      if (suffix == 0) {
+
+        // This means current frame is fooa*, so we have to first
+        // pop the current frame, then scan in parent frame:
+        if (currentFrame.ord == 0) {
+          throw NoMoreTermsException.INSTANCE;
+        }
+        currentFrame = stack[currentFrame.ord-1];
+        currentTransition = currentFrame.transition;
+
+        // Current (parent) frame is now foo*, so now we just scan
+        // until the lead suffix byte is > floorSuffixLeadEnd
+        //assert currentFrame.prefix == prefix-1;
+        //prefix = currentFrame.prefix;
+
+        // In case when we pop, and the parent block is not just prefix-1, e.g. in block 417* on
+        // its first term = floor prefix term 41[7-9], popping to block 4*:
+        prefix = currentFrame.prefix;
+
+        suffix = term.length - currentFrame.prefix;
+      } else {
+        // No need to pop; just scan in currentFrame:
+      }
+
+      // Now we scan until the lead suffix byte is > floorSuffixLeadEnd
+      scanFloor:
+      while (true) {
+        if (currentFrame.nextEnt == currentFrame.entCount) {
+          if (currentFrame.isLastInFloor == false) {
+            currentFrame.loadNextFloorBlock();
+          } else if (currentFrame.ord == 0) {
+            throw NoMoreTermsException.INSTANCE;
+          } else {
+            // Pop frame, which also means we've moved beyond this
+            // auto-prefix term:
+            currentFrame = stack[currentFrame.ord-1];
+            currentTransition = currentFrame.transition;
+
+            return popPushNext();
+          }
+        }
+        isSubBlock = currentFrame.next();
+        for(int i=0;i<suffix-1;i++) {
+          if (term.bytes[prefix+i] != currentFrame.suffixBytes[currentFrame.startBytePos+i]) {
+            break scanFloor;
+          }
+        }
+        if (currentFrame.suffix >= suffix && (currentFrame.suffixBytes[currentFrame.startBytePos+suffix-1]&0xff) > floorSuffixLeadEnd) {
+          // Done scanning: we are now on the first term after all
+          // terms matched by this auto-prefix term
+          break;
+        }
+      }
+    }
+
+    return isSubBlock;
+  }
+
+  // Only used internally when there are no more terms in next():
+  private static final class NoMoreTermsException extends RuntimeException {
+
+    // Only used internally when there are no more terms in next():
+    public static final NoMoreTermsException INSTANCE = new NoMoreTermsException();
+
+    private NoMoreTermsException() {
+    }
+
+    @Override
+    public Throwable fillInStackTrace() {
+      // Do nothing:
+      return this;
+    }    
+  }
+
+  @Override
+  public BytesRef next() throws IOException {
+    try {
+      return _next();
+    } catch (NoMoreTermsException eoi) {
+      // Provoke NPE if we are (illegally!) called again:
+      currentFrame = null;
+      return null;
+    }
+  }
+
+  private BytesRef _next() throws IOException {
+
+    boolean isSubBlock;
+
+    if (useAutoPrefixTerm) {
+      // If the current term was an auto-prefix term, we have to skip past it:
+      isSubBlock = skipPastLastAutoPrefixTerm();
+      assert useAutoPrefixTerm == false;
+    } else {
+      isSubBlock = popPushNext();
+    }
+
+    nextTerm:
+
+    while (true) {
+      assert currentFrame.transition == currentTransition;
+
+      int state;
+      int lastState;
+
+      // NOTE: suffix == 0 can only happen on the first term in a block, when
+      // there is a term exactly matching a prefix in the index.  If we
+      // could somehow re-org the code so we only checked this case immediately
+      // after pushing a frame...
+      if (currentFrame.suffix != 0) {
+
+        final byte[] suffixBytes = currentFrame.suffixBytes;
+
+        // This is the first byte of the suffix of the term we are now on:
+        final int label = suffixBytes[currentFrame.startBytePos] & 0xff;
+
+        if (label < currentTransition.min) {
+          // Common case: we are scanning terms in this block to "catch up" to
+          // current transition in the automaton:
+          int minTrans = currentTransition.min;
+          while (currentFrame.nextEnt < currentFrame.entCount) {
+            isSubBlock = currentFrame.next();
+            if ((suffixBytes[currentFrame.startBytePos] & 0xff) >= minTrans) {
+              continue nextTerm;
+            }
+          }
+
+          // End of frame:
+          isSubBlock = popPushNext();
+          continue nextTerm;
+        }
+
+        // Advance where we are in the automaton to match this label:
+
+        while (label > currentTransition.max) {
+          if (currentFrame.transitionIndex >= currentFrame.transitionCount-1) {
+            // Pop this frame: no further matches are possible because
+            // we've moved beyond what the max transition will allow
+            if (currentFrame.ord == 0) {
+              // Provoke NPE if we are (illegally!) called again:
+              currentFrame = null;
+              return null;
+            }
+            currentFrame = stack[currentFrame.ord-1];
+            currentTransition = currentFrame.transition;
+            isSubBlock = popPushNext();
+            continue nextTerm;
+          }
+          currentFrame.transitionIndex++;
+          automaton.getNextTransition(currentTransition);
+
+          if (label < currentTransition.min) {
+            int minTrans = currentTransition.min;
+            while (currentFrame.nextEnt < currentFrame.entCount) {
+              isSubBlock = currentFrame.next();
+              if ((suffixBytes[currentFrame.startBytePos] & 0xff) >= minTrans) {
+                continue nextTerm;
+              }
+            }
+
+            // End of frame:
+            isSubBlock = popPushNext();
+            continue nextTerm;
+          }
+        }
+
+        if (commonSuffix != null && !isSubBlock) {
+          final int termLen = currentFrame.prefix + currentFrame.suffix;
+          if (termLen < commonSuffix.length) {
+            // No match
+            isSubBlock = popPushNext();
+            continue nextTerm;
+          }
+
+          final byte[] commonSuffixBytes = commonSuffix.bytes;
+
+          final int lenInPrefix = commonSuffix.length - currentFrame.suffix;
+          assert commonSuffix.offset == 0;
+          int suffixBytesPos;
+          int commonSuffixBytesPos = 0;
+
+          if (lenInPrefix > 0) {
+            // A prefix of the common suffix overlaps with
+            // the suffix of the block prefix so we first
+            // test whether the prefix part matches:
+            final byte[] termBytes = term.bytes;
+            int termBytesPos = currentFrame.prefix - lenInPrefix;
+            assert termBytesPos >= 0;
+            final int termBytesPosEnd = currentFrame.prefix;
+            while (termBytesPos < termBytesPosEnd) {
+              if (termBytes[termBytesPos++] != commonSuffixBytes[commonSuffixBytesPos++]) {
+                isSubBlock = popPushNext();
+                continue nextTerm;
+              }
+            }
+            suffixBytesPos = currentFrame.startBytePos;
+          } else {
+            suffixBytesPos = currentFrame.startBytePos + currentFrame.suffix - commonSuffix.length;
+          }
+
+          // Test overlapping suffix part:
+          final int commonSuffixBytesPosEnd = commonSuffix.length;
+          while (commonSuffixBytesPos < commonSuffixBytesPosEnd) {
+            if (suffixBytes[suffixBytesPos++] != commonSuffixBytes[commonSuffixBytesPos++]) {
+              isSubBlock = popPushNext();
+              continue nextTerm;
+            }
+          }
+        }
+
+        // TODO: maybe we should do the same linear test
+        // that AutomatonTermsEnum does, so that if we
+        // reach a part of the automaton where .* is
+        // "temporarily" accepted, we just blindly .next()
+        // until the limit
+
+        // See if the term suffix matches the automaton:
+
+        // We know from above that the first byte in our suffix (label) matches
+        // the current transition, so we step from the 2nd byte
+        // in the suffix:
+        lastState = currentFrame.state;
+        state = currentTransition.dest;
+
+        int end = currentFrame.startBytePos + currentFrame.suffix;
+        for (int idx=currentFrame.startBytePos+1;idx<end;idx++) {
+          lastState = state;
+          state = runAutomaton.step(state, suffixBytes[idx] & 0xff);
+          if (state == -1) {
+            // No match
+            isSubBlock = popPushNext();
+            continue nextTerm;
+          }
+        }
+      } else {
+        state = currentFrame.state;
+        lastState = currentFrame.lastState;
+      }
+
+      if (isSubBlock) {
+        // Match!  Recurse:
+        copyTerm();
+        currentFrame = pushFrame(state);
+        currentTransition = currentFrame.transition;
+        currentFrame.lastState = lastState;
+      } else if (currentFrame.isAutoPrefixTerm) {
+        // We are on an auto-prefix term, meaning this term was compiled
+        // at indexing time, matching all terms sharing this prefix (or,
+        // a floor'd subset of them if that count was too high).  A
+        // prefix term represents a range of terms, so we now need to
+        // test whether, from the current state in the automaton, it
+        // accepts all terms in that range.  As long as it does, we can
+        // use this term and then later skip ahead past all terms in
+        // this range:
+        if (allowAutoPrefixTerms) {
+
+          if (currentFrame.floorSuffixLeadEnd == -1) {
+            // Simple prefix case
+            useAutoPrefixTerm = state == sinkState;
+          } else {
+            if (currentFrame.floorSuffixLeadStart == -1) {
+              // Must also accept the empty string in this case
+              if (automaton.isAccept(state)) {
+                useAutoPrefixTerm = acceptsSuffixRange(state, 0, currentFrame.floorSuffixLeadEnd);
+              }
+            } else {
+              useAutoPrefixTerm = acceptsSuffixRange(lastState, currentFrame.floorSuffixLeadStart, currentFrame.floorSuffixLeadEnd);
+            }
+          }
+
+          if (useAutoPrefixTerm) {
+            // All suffixes of this auto-prefix term are accepted by the automaton, so we can use it:
+            copyTerm();
+            currentFrame.termState.isRealTerm = false;
+            return term;
+          } else {
+            // We move onto the next term
+          }
+        } else {
+          // We are not allowed to use auto-prefix terms, so we just skip it
+        }
+      } else if (runAutomaton.isAccept(state)) {
+        copyTerm();
+        assert savedStartTerm == null || term.compareTo(savedStartTerm) > 0: "saveStartTerm=" + savedStartTerm.utf8ToString() + " term=" + term.utf8ToString();
+        return term;
+      } else {
+        // This term is a prefix of a term accepted by the automaton, but is not itself acceptd
+      }
+
+      isSubBlock = popPushNext();
+    }
+  }
+
+  private final Transition scratchTransition = new Transition();
+
+  /** Returns true if, from this state, the automaton accepts any suffix
+   *  starting with a label between start and end, inclusive.  We just
+   *  look for a transition, matching this range, to the sink state.  */
+  private boolean acceptsSuffixRange(int state, int start, int end) {
+
+    int count = automaton.initTransition(state, scratchTransition);
+    for(int i=0;i<count;i++) {
+      automaton.getNextTransition(scratchTransition);
+      if (start >= scratchTransition.min && end <= scratchTransition.max && scratchTransition.dest == sinkState) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  // for debugging
+  @SuppressWarnings("unused")
+  static String brToString(BytesRef b) {
+    try {
+      return b.utf8ToString() + " " + b;
+    } catch (Throwable t) {
+      // If BytesRef isn't actually UTF8, or it's eg a
+      // prefix of UTF8 that ends mid-unicode-char, we
+      // fallback to hex:
+      return b.toString();
+    }
+  }
+
+  private void copyTerm() {
+    final int len = currentFrame.prefix + currentFrame.suffix;
+    if (term.bytes.length < len) {
+      term.bytes = ArrayUtil.grow(term.bytes, len);
+    }
+    System.arraycopy(currentFrame.suffixBytes, currentFrame.startBytePos, term.bytes, currentFrame.prefix, currentFrame.suffix);
+    term.length = len;
+  }
+
+  @Override
+  public boolean seekExact(BytesRef text) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void seekExact(long ord) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public long ord() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public SeekStatus seekCeil(BytesRef text) {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedIntersectTermsEnumFrame.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedIntersectTermsEnumFrame.java
new file mode 100644
index 0000000..d0a2d7f
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedIntersectTermsEnumFrame.java
@@ -0,0 +1,362 @@
+package org.apache.lucene.codecs.encrypted.blocktree;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.BlockTermState;
+import org.apache.lucene.index.IndexOptions;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.automaton.Transition;
+import org.apache.lucene.util.fst.FST;
+
+import java.io.IOException;
+
+// TODO: can we share this with the frame in STE?
+final class EncryptedIntersectTermsEnumFrame {
+  final int ord;
+  long fp;
+  long fpOrig;
+  long fpEnd;
+  long lastSubFP;
+
+  // private static boolean DEBUG = EncryptedIntersectTermsEnum.DEBUG;
+
+  // State in automaton
+  int state;
+
+  // State just before the last label
+  int lastState;
+
+  int metaDataUpto;
+
+  byte[] suffixBytes = new byte[128];
+  final ByteArrayDataInput suffixesReader = new ByteArrayDataInput();
+
+  byte[] statBytes = new byte[64];
+  final ByteArrayDataInput statsReader = new ByteArrayDataInput();
+
+  byte[] floorData = new byte[32];
+  final ByteArrayDataInput floorDataReader = new ByteArrayDataInput();
+
+  // Length of prefix shared by all terms in this block
+  int prefix;
+
+  // Number of entries (term or sub-block) in this block
+  int entCount;
+
+  // Which term we will next read
+  int nextEnt;
+
+  // True if this block is either not a floor block,
+  // or, it's the last sub-block of a floor block
+  boolean isLastInFloor;
+
+  // True if all entries are terms
+  boolean isLeafBlock;
+
+  int numFollowFloorBlocks;
+  int nextFloorLabel;
+        
+  final Transition transition = new Transition();
+  int transitionIndex;
+  int transitionCount;
+
+  final boolean versionAutoPrefix;
+
+  FST.Arc<BytesRef> arc;
+
+  final BlockTermState termState;
+  
+  // metadata buffer, holding monotonic values
+  final long[] longs;
+
+  // metadata buffer, holding general values
+  byte[] bytes = new byte[32];
+
+  final ByteArrayDataInput bytesReader = new ByteArrayDataInput();
+
+  // Cumulative output so far
+  BytesRef outputPrefix;
+
+  int startBytePos;
+  int suffix;
+
+  // When we are on an auto-prefix term this is the starting lead byte
+  // of the suffix (e.g. 'a' for the foo[a-m]* case):
+  int floorSuffixLeadStart;
+
+  // When we are on an auto-prefix term this is the ending lead byte
+  // of the suffix (e.g. 'm' for the foo[a-m]* case):
+  int floorSuffixLeadEnd;
+
+  // True if the term we are currently on is an auto-prefix term:
+  boolean isAutoPrefixTerm;
+
+  private final EncryptedIntersectTermsEnum ite;
+
+  public EncryptedIntersectTermsEnumFrame(EncryptedIntersectTermsEnum ite, int ord) throws IOException {
+    this.ite = ite;
+    this.ord = ord;
+    this.termState = ite.fr.parent.postingsReader.newTermState();
+    this.termState.totalTermFreq = -1;
+    this.longs = new long[ite.fr.longsSize];
+    this.versionAutoPrefix = ite.fr.parent.anyAutoPrefixTerms;
+  }
+
+  void loadNextFloorBlock() throws IOException {
+    assert numFollowFloorBlocks > 0: "nextFloorLabel=" + nextFloorLabel;
+
+    do {
+      fp = fpOrig + (floorDataReader.readVLong() >>> 1);
+      numFollowFloorBlocks--;
+      if (numFollowFloorBlocks != 0) {
+        nextFloorLabel = floorDataReader.readByte() & 0xff;
+      } else {
+        nextFloorLabel = 256;
+      }
+    } while (numFollowFloorBlocks != 0 && nextFloorLabel <= transition.min);
+
+    load(null);
+  }
+
+  public void setState(int state) {
+    this.state = state;
+    transitionIndex = 0;
+    transitionCount = ite.automaton.getNumTransitions(state);
+    if (transitionCount != 0) {
+      ite.automaton.initTransition(state, transition);
+      ite.automaton.getNextTransition(transition);
+    } else {
+
+      // Must set min to -1 so the "label < min" check never falsely triggers:
+      transition.min = -1;
+
+      // Must set max to -1 so we immediately realize we need to step to the next transition and then pop this frame:
+      transition.max = -1;
+    }
+  }
+
+  void load(BytesRef frameIndexData) throws IOException {
+    if (frameIndexData != null) {
+      floorDataReader.reset(frameIndexData.bytes, frameIndexData.offset, frameIndexData.length);
+      // Skip first long -- has redundant fp, hasTerms
+      // flag, isFloor flag
+      final long code = floorDataReader.readVLong();
+      if ((code & EncryptedBlockTreeTermsReader.OUTPUT_FLAG_IS_FLOOR) != 0) {
+        // Floor frame
+        numFollowFloorBlocks = floorDataReader.readVInt();
+        nextFloorLabel = floorDataReader.readByte() & 0xff;
+
+        // If current state is not accept, and has transitions, we must process
+        // first block in case it has empty suffix:
+        if (ite.runAutomaton.isAccept(state) == false && transitionCount != 0) {
+          // Maybe skip floor blocks:
+          assert transitionIndex == 0: "transitionIndex=" + transitionIndex;
+          while (numFollowFloorBlocks != 0 && nextFloorLabel <= transition.min) {
+            fp = fpOrig + (floorDataReader.readVLong() >>> 1);
+            numFollowFloorBlocks--;
+            if (numFollowFloorBlocks != 0) {
+              nextFloorLabel = floorDataReader.readByte() & 0xff;
+            } else {
+              nextFloorLabel = 256;
+            }
+          }
+        }
+      }
+    }
+
+    ite.in.seek(fp);
+    int code = ite.in.readVInt();
+    entCount = code >>> 1;
+    assert entCount > 0;
+    isLastInFloor = (code & 1) != 0;
+
+    // term suffixes:
+    code = ite.in.readVInt();
+    isLeafBlock = (code & 1) != 0;
+    int numBytes = code >>> 1; // decode size of encrypted suffixes byte[]
+    if (suffixBytes.length < numBytes) {
+      suffixBytes = new byte[ArrayUtil.oversize(numBytes, 1)];
+    }
+    ite.in.readBytes(suffixBytes, 0, numBytes); // read encrypted suffixes byte[]
+    int read = 0;
+    try {
+      // we can reuse suffixBytes as output since the decrypt method is copy-safe
+      read = ite.decipher.doFinal(suffixBytes, 0, numBytes, suffixBytes, 0);
+    } catch (Exception e) {
+      throw new IOException("Deciphering of term suffix block failed", e);
+    }
+    suffixesReader.reset(suffixBytes, 0, read);
+
+    // stats
+    numBytes = ite.in.readVInt();
+    if (statBytes.length < numBytes) {
+      statBytes = new byte[ArrayUtil.oversize(numBytes, 1)];
+    }
+    ite.in.readBytes(statBytes, 0, numBytes);
+    statsReader.reset(statBytes, 0, numBytes);
+    metaDataUpto = 0;
+
+    termState.termBlockOrd = 0;
+    nextEnt = 0;
+         
+    // metadata
+    numBytes = ite.in.readVInt();
+    if (bytes.length < numBytes) {
+      bytes = new byte[ArrayUtil.oversize(numBytes, 1)];
+    }
+    ite.in.readBytes(bytes, 0, numBytes);
+    bytesReader.reset(bytes, 0, numBytes);
+
+    if (!isLastInFloor) {
+      // Sub-blocks of a single floor block are always
+      // written one after another -- tail recurse:
+      fpEnd = ite.in.getFilePointer();
+    }
+
+    // Necessary in case this ord previously was an auto-prefix
+    // term but now we recurse to a new leaf block
+    isAutoPrefixTerm = false;
+  }
+
+  // TODO: maybe add scanToLabel; should give perf boost
+
+  // Decodes next entry; returns true if it's a sub-block
+  public boolean next() {
+    if (isLeafBlock) {
+      nextLeaf();
+      return false;
+    } else {
+      return nextNonLeaf();
+    }
+  }
+
+  public void nextLeaf() {
+    assert nextEnt != -1 && nextEnt < entCount: "nextEnt=" + nextEnt + " entCount=" + entCount + " fp=" + fp;
+    nextEnt++;
+    suffix = suffixesReader.readVInt();
+    startBytePos = suffixesReader.getPosition();
+    suffixesReader.skipBytes(suffix);
+  }
+
+  public boolean nextNonLeaf() {
+    assert nextEnt != -1 && nextEnt < entCount: "nextEnt=" + nextEnt + " entCount=" + entCount + " fp=" + fp;
+    nextEnt++;
+    final int code = suffixesReader.readVInt();
+    if (versionAutoPrefix == false) {
+      suffix = code >>> 1;
+      startBytePos = suffixesReader.getPosition();
+      suffixesReader.skipBytes(suffix);
+      if ((code & 1) == 0) {
+        // A normal term
+        termState.termBlockOrd++;
+        return false;
+      } else {
+        // A sub-block; make sub-FP absolute:
+        lastSubFP = fp - suffixesReader.readVLong();
+        return true;
+      }
+    } else {
+      suffix = code >>> 2;
+      startBytePos = suffixesReader.getPosition();
+      suffixesReader.skipBytes(suffix);
+      switch (code & 3) {
+      case 0:
+        // A normal term
+        isAutoPrefixTerm = false;
+        termState.termBlockOrd++;
+        return false;
+      case 1:
+        // A sub-block; make sub-FP absolute:
+        isAutoPrefixTerm = false;
+        lastSubFP = fp - suffixesReader.readVLong();
+        return true;
+      case 2:
+        // A normal prefix term, suffix leads with empty string
+        floorSuffixLeadStart = -1;
+        termState.termBlockOrd++;
+        floorSuffixLeadEnd = suffixesReader.readByte() & 0xff;
+        if (floorSuffixLeadEnd == 0xff) {
+          floorSuffixLeadEnd = -1;
+        }
+        isAutoPrefixTerm = true;
+        return false;
+      case 3:
+        // A floor'd prefix term, suffix leads with real byte
+        if (suffix == 0) {
+          // TODO: this is messy, but necessary because we are an auto-prefix term, but our suffix is the empty string here, so we have to
+          // look at the parent block to get the lead suffix byte:
+          assert ord > 0;
+          EncryptedIntersectTermsEnumFrame parent = ite.stack[ord-1];
+          floorSuffixLeadStart = parent.suffixBytes[parent.startBytePos+parent.suffix-1] & 0xff;
+        } else {
+          floorSuffixLeadStart = suffixBytes[startBytePos+suffix-1] & 0xff;
+        }
+        termState.termBlockOrd++;
+        isAutoPrefixTerm = true;
+        floorSuffixLeadEnd = suffixesReader.readByte() & 0xff;
+        return false;
+      default:
+        // Silly javac:
+        assert false;
+        return false;
+      }
+    }
+  }
+
+  public int getTermBlockOrd() {
+    return isLeafBlock ? nextEnt : termState.termBlockOrd;
+  }
+
+  public void decodeMetaData() throws IOException {
+
+    // lazily catch up on metadata decode:
+    final int limit = getTermBlockOrd();
+    boolean absolute = metaDataUpto == 0;
+    assert limit > 0;
+
+    // TODO: better API would be "jump straight to term=N"???
+    while (metaDataUpto < limit) {
+
+      // TODO: we could make "tiers" of metadata, ie,
+      // decode docFreq/totalTF but don't decode postings
+      // metadata; this way caller could get
+      // docFreq/totalTF w/o paying decode cost for
+      // postings
+
+      // TODO: if docFreq were bulk decoded we could
+      // just skipN here:
+
+      // stats
+      termState.docFreq = statsReader.readVInt();
+      if (ite.fr.fieldInfo.getIndexOptions() != IndexOptions.DOCS) {
+        termState.totalTermFreq = termState.docFreq + statsReader.readVLong();
+      }
+      // metadata 
+      for (int i = 0; i < ite.fr.longsSize; i++) {
+        longs[i] = bytesReader.readVLong();
+      }
+      ite.fr.parent.postingsReader.decodeTerm(longs, bytesReader, ite.fr.fieldInfo, termState, absolute);
+
+      metaDataUpto++;
+      absolute = false;
+    }
+    termState.termBlockOrd = metaDataUpto;
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedSegmentTermsEnum.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedSegmentTermsEnum.java
new file mode 100644
index 0000000..f498305
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedSegmentTermsEnum.java
@@ -0,0 +1,1048 @@
+package org.apache.lucene.codecs.encrypted.blocktree;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.BlockTermState;
+import org.apache.lucene.index.PostingsEnum;
+import org.apache.lucene.index.TermState;
+import org.apache.lucene.index.TermsEnum;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.BytesRefBuilder;
+import org.apache.lucene.util.RamUsageEstimator;
+import org.apache.lucene.util.fst.FST;
+import org.apache.lucene.util.fst.Util;
+
+import javax.crypto.Cipher;
+import java.io.IOException;
+import java.io.PrintStream;
+
+/** Iterates through terms in this field.  This implementation skips
+ *  any auto-prefix terms it encounters. */
+
+final class EncryptedSegmentTermsEnum extends TermsEnum {
+
+  // Lazy init:
+  IndexInput in;
+
+  private EncryptedSegmentTermsEnumFrame[] stack;
+  private final EncryptedSegmentTermsEnumFrame staticFrame;
+  EncryptedSegmentTermsEnumFrame currentFrame;
+  boolean termExists;
+  final EncryptedFieldReader fr;
+  // decipher instance must be local to a TermsEnum to ensure thread safety
+  final Cipher decipher;
+
+  private int targetBeforeCurrentLength;
+
+  //static boolean DEBUG = BlockTreeTermsWriter.DEBUG;
+
+  private final ByteArrayDataInput scratchReader = new ByteArrayDataInput();
+
+  // What prefix of the current term was present in the index; when we only next() through the index, this stays at 0.  It's only set when
+  // we seekCeil/Exact:
+  private int validIndexPrefix;
+
+  // assert only:
+  private boolean eof;
+
+  final BytesRefBuilder term = new BytesRefBuilder();
+  private final FST.BytesReader fstReader;
+
+  @SuppressWarnings({"rawtypes","unchecked"}) private FST.Arc<BytesRef>[] arcs = new FST.Arc[1];
+
+  public EncryptedSegmentTermsEnum(EncryptedFieldReader fr) throws IOException {
+    this.fr = fr;
+    this.decipher = fr.parent.cipherFactory.newDecipherInstance(fr.parent.keyVersion);
+
+    // if (DEBUG) {
+    //   System.out.println("BTTR.init seg=" + fr.parent.segment);
+    // }
+    stack = new EncryptedSegmentTermsEnumFrame[0];
+        
+    // Used to hold seek by TermState, or cached seek
+    staticFrame = new EncryptedSegmentTermsEnumFrame(this, -1);
+
+    if (fr.index == null) {
+      fstReader = null;
+    } else {
+      fstReader = fr.index.getBytesReader();
+    }
+
+    // Init w/ root block; don't use index since it may
+    // not (and need not) have been loaded
+    for(int arcIdx=0;arcIdx<arcs.length;arcIdx++) {
+      arcs[arcIdx] = new FST.Arc<>();
+    }
+
+    currentFrame = staticFrame;
+    final FST.Arc<BytesRef> arc;
+    if (fr.index != null) {
+      arc = fr.index.getFirstArc(arcs[0]);
+      // Empty string prefix must have an output in the index!
+      assert arc.isFinal();
+    } else {
+      arc = null;
+    }
+    //currentFrame = pushFrame(arc, rootCode, 0);
+    //currentFrame.loadBlock();
+    validIndexPrefix = 0;
+    // if (DEBUG) {
+    //   System.out.println("init frame state " + currentFrame.ord);
+    //   printSeekState();
+    // }
+
+    //System.out.println();
+    // computeBlockStats().print(System.out);
+  }
+      
+  // Not private to avoid synthetic access$NNN methods
+  void initIndexInput() {
+    if (this.in == null) {
+      this.in = fr.parent.termsIn.clone();
+    }
+  }
+
+  /** Runs next() through the entire terms dict,
+   *  computing aggregate statistics. */
+  public Stats computeBlockStats() throws IOException {
+
+    // TODO: add total auto-prefix term count
+
+    Stats stats = new Stats(fr.parent.segment, fr.fieldInfo.name);
+    if (fr.index != null) {
+      stats.indexNumBytes = fr.index.ramBytesUsed();
+    }
+        
+    currentFrame = staticFrame;
+    FST.Arc<BytesRef> arc;
+    if (fr.index != null) {
+      arc = fr.index.getFirstArc(arcs[0]);
+      // Empty string prefix must have an output in the index!
+      assert arc.isFinal();
+    } else {
+      arc = null;
+    }
+
+    // Empty string prefix must have an output in the
+    // index!
+    currentFrame = pushFrame(arc, fr.rootCode, 0);
+    currentFrame.fpOrig = currentFrame.fp;
+    currentFrame.loadBlock();
+    validIndexPrefix = 0;
+
+    stats.startBlock(currentFrame, !currentFrame.isLastInFloor);
+
+    allTerms:
+    while (true) {
+
+      // Pop finished blocks
+      while (currentFrame.nextEnt == currentFrame.entCount) {
+        stats.endBlock(currentFrame);
+        if (!currentFrame.isLastInFloor) {
+          // Advance to next floor block
+          currentFrame.loadNextFloorBlock();
+          stats.startBlock(currentFrame, true);
+          break;
+        } else {
+          if (currentFrame.ord == 0) {
+            break allTerms;
+          }
+          final long lastFP = currentFrame.fpOrig;
+          currentFrame = stack[currentFrame.ord-1];
+          assert lastFP == currentFrame.lastSubFP;
+          // if (DEBUG) {
+          //   System.out.println("  reset validIndexPrefix=" + validIndexPrefix);
+          // }
+        }
+      }
+
+      while(true) {
+        if (currentFrame.next()) {
+          // Push to new block:
+          currentFrame = pushFrame(null, currentFrame.lastSubFP, term.length());
+          currentFrame.fpOrig = currentFrame.fp;
+          // This is a "next" frame -- even if it's
+          // floor'd we must pretend it isn't so we don't
+          // try to scan to the right floor frame:
+          currentFrame.loadBlock();
+          stats.startBlock(currentFrame, !currentFrame.isLastInFloor);
+        } else {
+          stats.term(term.get());
+          break;
+        }
+      }
+    }
+
+    stats.finish();
+
+    // Put root frame back:
+    currentFrame = staticFrame;
+    if (fr.index != null) {
+      arc = fr.index.getFirstArc(arcs[0]);
+      // Empty string prefix must have an output in the index!
+      assert arc.isFinal();
+    } else {
+      arc = null;
+    }
+    currentFrame = pushFrame(arc, fr.rootCode, 0);
+    currentFrame.rewind();
+    currentFrame.loadBlock();
+    validIndexPrefix = 0;
+    term.clear();
+
+    return stats;
+  }
+
+  private EncryptedSegmentTermsEnumFrame getFrame(int ord) throws IOException {
+    if (ord >= stack.length) {
+      final EncryptedSegmentTermsEnumFrame[] next = new EncryptedSegmentTermsEnumFrame[ArrayUtil.oversize(1+ord, RamUsageEstimator.NUM_BYTES_OBJECT_REF)];
+      System.arraycopy(stack, 0, next, 0, stack.length);
+      for(int stackOrd=stack.length;stackOrd<next.length;stackOrd++) {
+        next[stackOrd] = new EncryptedSegmentTermsEnumFrame(this, stackOrd);
+      }
+      stack = next;
+    }
+    assert stack[ord].ord == ord;
+    return stack[ord];
+  }
+
+  private FST.Arc<BytesRef> getArc(int ord) {
+    if (ord >= arcs.length) {
+      @SuppressWarnings({"rawtypes","unchecked"}) final FST.Arc<BytesRef>[] next =
+      new FST.Arc[ArrayUtil.oversize(1+ord, RamUsageEstimator.NUM_BYTES_OBJECT_REF)];
+      System.arraycopy(arcs, 0, next, 0, arcs.length);
+      for(int arcOrd=arcs.length;arcOrd<next.length;arcOrd++) {
+        next[arcOrd] = new FST.Arc<>();
+      }
+      arcs = next;
+    }
+    return arcs[ord];
+  }
+
+  // Pushes a frame we seek'd to
+  EncryptedSegmentTermsEnumFrame pushFrame(FST.Arc<BytesRef> arc, BytesRef frameData, int length) throws IOException {
+    scratchReader.reset(frameData.bytes, frameData.offset, frameData.length);
+    final long code = scratchReader.readVLong();
+    final long fpSeek = code >>> EncryptedBlockTreeTermsReader.OUTPUT_FLAGS_NUM_BITS;
+    final EncryptedSegmentTermsEnumFrame f = getFrame(1+currentFrame.ord);
+    f.hasTerms = (code & EncryptedBlockTreeTermsReader.OUTPUT_FLAG_HAS_TERMS) != 0;
+    f.hasTermsOrig = f.hasTerms;
+    f.isFloor = (code & EncryptedBlockTreeTermsReader.OUTPUT_FLAG_IS_FLOOR) != 0;
+    if (f.isFloor) {
+      f.setFloorData(scratchReader, frameData);
+    }
+    pushFrame(arc, fpSeek, length);
+
+    return f;
+  }
+
+  // Pushes next'd frame or seek'd frame; we later
+  // lazy-load the frame only when needed
+  EncryptedSegmentTermsEnumFrame pushFrame(FST.Arc<BytesRef> arc, long fp, int length) throws IOException {
+    final EncryptedSegmentTermsEnumFrame f = getFrame(1+currentFrame.ord);
+    f.arc = arc;
+    if (f.fpOrig == fp && f.nextEnt != -1) {
+      //if (DEBUG) System.out.println("      push reused frame ord=" + f.ord + " fp=" + f.fp + " isFloor?=" + f.isFloor + " hasTerms=" + f.hasTerms + " pref=" + term + " nextEnt=" + f.nextEnt + " targetBeforeCurrentLength=" + targetBeforeCurrentLength + " term.length=" + term.length + " vs prefix=" + f.prefix);
+      //if (f.prefix > targetBeforeCurrentLength) {
+      if (f.ord > targetBeforeCurrentLength) {
+        f.rewind();
+      } else {
+        // if (DEBUG) {
+        //   System.out.println("        skip rewind!");
+        // }
+      }
+      assert length == f.prefix;
+    } else {
+      f.nextEnt = -1;
+      f.prefix = length;
+      f.state.termBlockOrd = 0;
+      f.fpOrig = f.fp = fp;
+      f.lastSubFP = -1;
+      // if (DEBUG) {
+      //   final int sav = term.length;
+      //   term.length = length;
+      //   System.out.println("      push new frame ord=" + f.ord + " fp=" + f.fp + " hasTerms=" + f.hasTerms + " isFloor=" + f.isFloor + " pref=" + brToString(term));
+      //   term.length = sav;
+      // }
+    }
+
+    return f;
+  }
+
+  // asserts only
+  private boolean clearEOF() {
+    eof = false;
+    return true;
+  }
+
+  // asserts only
+  private boolean setEOF() {
+    eof = true;
+    return true;
+  }
+
+  /*
+  // for debugging
+  @SuppressWarnings("unused")
+  static String brToString(BytesRef b) {
+    try {
+      return b.utf8ToString() + " " + b;
+    } catch (Throwable t) {
+      // If BytesRef isn't actually UTF8, or it's eg a
+      // prefix of UTF8 that ends mid-unicode-char, we
+      // fallback to hex:
+      return b.toString();
+    }
+  }
+
+  // for debugging
+  @SuppressWarnings("unused")
+  static String brToString(BytesRefBuilder b) {
+    return brToString(b.get());
+  }
+  */
+
+  @Override
+  public boolean seekExact(BytesRef target) throws IOException {
+
+    if (fr.index == null) {
+      throw new IllegalStateException("terms index was not loaded");
+    }
+
+    term.grow(1 + target.length);
+
+    assert clearEOF();
+
+    // if (DEBUG) {
+    //   System.out.println("\nBTTR.seekExact seg=" + fr.parent.segment + " target=" + fr.fieldInfo.name + ":" + brToString(target) + " current=" + brToString(term) + " (exists?=" + termExists + ") validIndexPrefix=" + validIndexPrefix);
+    //   printSeekState(System.out);
+    // }
+
+    FST.Arc<BytesRef> arc;
+    int targetUpto;
+    BytesRef output;
+
+    targetBeforeCurrentLength = currentFrame.ord;
+
+    if (currentFrame != staticFrame) {
+
+      // We are already seek'd; find the common
+      // prefix of new seek term vs current term and
+      // re-use the corresponding seek state.  For
+      // example, if app first seeks to foobar, then
+      // seeks to foobaz, we can re-use the seek state
+      // for the first 5 bytes.
+
+      // if (DEBUG) {
+      //   System.out.println("  re-use current seek state validIndexPrefix=" + validIndexPrefix);
+      // }
+
+      arc = arcs[0];
+      assert arc.isFinal();
+      output = arc.output;
+      targetUpto = 0;
+          
+      EncryptedSegmentTermsEnumFrame lastFrame = stack[0];
+      assert validIndexPrefix <= term.length();
+
+      final int targetLimit = Math.min(target.length, validIndexPrefix);
+
+      int cmp = 0;
+
+      // TODO: reverse vLong byte order for better FST
+      // prefix output sharing
+
+      // First compare up to valid seek frames:
+      while (targetUpto < targetLimit) {
+        cmp = (term.byteAt(targetUpto)&0xFF) - (target.bytes[target.offset + targetUpto]&0xFF);
+        // if (DEBUG) {
+        //    System.out.println("    cycle targetUpto=" + targetUpto + " (vs limit=" + targetLimit + ") cmp=" + cmp + " (targetLabel=" + (char) (target.bytes[target.offset + targetUpto]) + " vs termLabel=" + (char) (term.bytes[targetUpto]) + ")"   + " arc.output=" + arc.output + " output=" + output);
+        // }
+        if (cmp != 0) {
+          break;
+        }
+        arc = arcs[1+targetUpto];
+        assert arc.label == (target.bytes[target.offset + targetUpto] & 0xFF): "arc.label=" + (char) arc.label + " targetLabel=" + (char) (target.bytes[target.offset + targetUpto] & 0xFF);
+        if (arc.output != EncryptedBlockTreeTermsReader.NO_OUTPUT) {
+          output = EncryptedBlockTreeTermsReader.FST_OUTPUTS.add(output, arc.output);
+        }
+        if (arc.isFinal()) {
+          lastFrame = stack[1+lastFrame.ord];
+        }
+        targetUpto++;
+      }
+
+      if (cmp == 0) {
+        final int targetUptoMid = targetUpto;
+
+        // Second compare the rest of the term, but
+        // don't save arc/output/frame; we only do this
+        // to find out if the target term is before,
+        // equal or after the current term
+        final int targetLimit2 = Math.min(target.length, term.length());
+        while (targetUpto < targetLimit2) {
+          cmp = (term.byteAt(targetUpto)&0xFF) - (target.bytes[target.offset + targetUpto]&0xFF);
+          // if (DEBUG) {
+          //    System.out.println("    cycle2 targetUpto=" + targetUpto + " (vs limit=" + targetLimit + ") cmp=" + cmp + " (targetLabel=" + (char) (target.bytes[target.offset + targetUpto]) + " vs termLabel=" + (char) (term.bytes[targetUpto]) + ")");
+          // }
+          if (cmp != 0) {
+            break;
+          }
+          targetUpto++;
+        }
+
+        if (cmp == 0) {
+          cmp = term.length() - target.length;
+        }
+        targetUpto = targetUptoMid;
+      }
+
+      if (cmp < 0) {
+        // Common case: target term is after current
+        // term, ie, app is seeking multiple terms
+        // in sorted order
+        // if (DEBUG) {
+        //   System.out.println("  target is after current (shares prefixLen=" + targetUpto + "); frame.ord=" + lastFrame.ord);
+        // }
+        currentFrame = lastFrame;
+
+      } else if (cmp > 0) {
+        // Uncommon case: target term
+        // is before current term; this means we can
+        // keep the currentFrame but we must rewind it
+        // (so we scan from the start)
+        targetBeforeCurrentLength = lastFrame.ord;
+        // if (DEBUG) {
+        //   System.out.println("  target is before current (shares prefixLen=" + targetUpto + "); rewind frame ord=" + lastFrame.ord);
+        // }
+        currentFrame = lastFrame;
+        currentFrame.rewind();
+      } else {
+        // Target is exactly the same as current term
+        assert term.length() == target.length;
+        if (termExists) {
+          // if (DEBUG) {
+          //   System.out.println("  target is same as current; return true");
+          // }
+          return true;
+        } else {
+          // if (DEBUG) {
+          //   System.out.println("  target is same as current but term doesn't exist");
+          // }
+        }
+        //validIndexPrefix = currentFrame.depth;
+        //term.length = target.length;
+        //return termExists;
+      }
+
+    } else {
+
+      targetBeforeCurrentLength = -1;
+      arc = fr.index.getFirstArc(arcs[0]);
+
+      // Empty string prefix must have an output (block) in the index!
+      assert arc.isFinal();
+      assert arc.output != null;
+
+      // if (DEBUG) {
+      //   System.out.println("    no seek state; push root frame");
+      // }
+
+      output = arc.output;
+
+      currentFrame = staticFrame;
+
+      //term.length = 0;
+      targetUpto = 0;
+      currentFrame = pushFrame(arc, EncryptedBlockTreeTermsReader.FST_OUTPUTS.add(output, arc.nextFinalOutput), 0);
+    }
+
+    // if (DEBUG) {
+    //   System.out.println("  start index loop targetUpto=" + targetUpto + " output=" + output + " currentFrame.ord=" + currentFrame.ord + " targetBeforeCurrentLength=" + targetBeforeCurrentLength);
+    // }
+
+    // We are done sharing the common prefix with the incoming target and where we are currently seek'd; now continue walking the index:
+    while (targetUpto < target.length) {
+
+      final int targetLabel = target.bytes[target.offset + targetUpto] & 0xFF;
+
+      final FST.Arc<BytesRef> nextArc = fr.index.findTargetArc(targetLabel, arc, getArc(1+targetUpto), fstReader);
+
+      if (nextArc == null) {
+
+        // Index is exhausted
+        // if (DEBUG) {
+        //   System.out.println("    index: index exhausted label=" + ((char) targetLabel) + " " + toHex(targetLabel));
+        // }
+            
+        validIndexPrefix = currentFrame.prefix;
+        //validIndexPrefix = targetUpto;
+
+        currentFrame.scanToFloorFrame(target);
+
+        if (!currentFrame.hasTerms) {
+          termExists = false;
+          term.setByteAt(targetUpto, (byte) targetLabel);
+          term.setLength(1+targetUpto);
+          // if (DEBUG) {
+          //   System.out.println("  FAST NOT_FOUND term=" + brToString(term));
+          // }
+          return false;
+        }
+
+        currentFrame.loadBlock();
+
+        final SeekStatus result = currentFrame.scanToTerm(target, true);            
+        if (result == SeekStatus.FOUND) {
+          // if (DEBUG) {
+          //   System.out.println("  return FOUND term=" + term.utf8ToString() + " " + term);
+          // }
+          return true;
+        } else {
+          // if (DEBUG) {
+          //   System.out.println("  got " + result + "; return NOT_FOUND term=" + brToString(term));
+          // }
+          return false;
+        }
+      } else {
+        // Follow this arc
+        arc = nextArc;
+        term.setByteAt(targetUpto, (byte) targetLabel);
+        // Aggregate output as we go:
+        assert arc.output != null;
+        if (arc.output != EncryptedBlockTreeTermsReader.NO_OUTPUT) {
+          output = EncryptedBlockTreeTermsReader.FST_OUTPUTS.add(output, arc.output);
+        }
+
+        // if (DEBUG) {
+        //   System.out.println("    index: follow label=" + toHex(target.bytes[target.offset + targetUpto]&0xff) + " arc.output=" + arc.output + " arc.nfo=" + arc.nextFinalOutput);
+        // }
+        targetUpto++;
+
+        if (arc.isFinal()) {
+          //if (DEBUG) System.out.println("    arc is final!");
+          currentFrame = pushFrame(arc, EncryptedBlockTreeTermsReader.FST_OUTPUTS.add(output, arc.nextFinalOutput), targetUpto);
+          //if (DEBUG) System.out.println("    curFrame.ord=" + currentFrame.ord + " hasTerms=" + currentFrame.hasTerms);
+        }
+      }
+    }
+
+    //validIndexPrefix = targetUpto;
+    validIndexPrefix = currentFrame.prefix;
+
+    currentFrame.scanToFloorFrame(target);
+
+    // Target term is entirely contained in the index:
+    if (!currentFrame.hasTerms) {
+      termExists = false;
+      term.setLength(targetUpto);
+      // if (DEBUG) {
+      //   System.out.println("  FAST NOT_FOUND term=" + brToString(term));
+      // }
+      return false;
+    }
+
+    currentFrame.loadBlock();
+
+    final SeekStatus result = currentFrame.scanToTerm(target, true);            
+    if (result == SeekStatus.FOUND) {
+      // if (DEBUG) {
+      //   System.out.println("  return FOUND term=" + term.utf8ToString() + " " + term);
+      // }
+      return true;
+    } else {
+      // if (DEBUG) {
+      //   System.out.println("  got result " + result + "; return NOT_FOUND term=" + term.utf8ToString());
+      // }
+
+      return false;
+    }
+  }
+
+  @Override
+  public SeekStatus seekCeil(BytesRef target) throws IOException {
+
+    if (fr.index == null) {
+      throw new IllegalStateException("terms index was not loaded");
+    }
+
+    term.grow(1 + target.length);
+
+    assert clearEOF();
+
+    // if (DEBUG) {
+    //   System.out.println("\nBTTR.seekCeil seg=" + fr.parent.segment + " target=" + fr.fieldInfo.name + ":" + brToString(target) + " " + target + " current=" + brToString(term) + " (exists?=" + termExists + ") validIndexPrefix=  " + validIndexPrefix);
+    //   printSeekState(System.out);
+    // }
+
+    FST.Arc<BytesRef> arc;
+    int targetUpto;
+    BytesRef output;
+
+    targetBeforeCurrentLength = currentFrame.ord;
+
+    if (currentFrame != staticFrame) {
+
+      // We are already seek'd; find the common
+      // prefix of new seek term vs current term and
+      // re-use the corresponding seek state.  For
+      // example, if app first seeks to foobar, then
+      // seeks to foobaz, we can re-use the seek state
+      // for the first 5 bytes.
+
+      //if (DEBUG) {
+      //System.out.println("  re-use current seek state validIndexPrefix=" + validIndexPrefix);
+      //}
+
+      arc = arcs[0];
+      assert arc.isFinal();
+      output = arc.output;
+      targetUpto = 0;
+          
+      EncryptedSegmentTermsEnumFrame lastFrame = stack[0];
+      assert validIndexPrefix <= term.length();
+
+      final int targetLimit = Math.min(target.length, validIndexPrefix);
+
+      int cmp = 0;
+
+      // TODO: we should write our vLong backwards (MSB
+      // first) to get better sharing from the FST
+
+      // First compare up to valid seek frames:
+      while (targetUpto < targetLimit) {
+        cmp = (term.byteAt(targetUpto)&0xFF) - (target.bytes[target.offset + targetUpto]&0xFF);
+        //if (DEBUG) {
+        //System.out.println("    cycle targetUpto=" + targetUpto + " (vs limit=" + targetLimit + ") cmp=" + cmp + " (targetLabel=" + (char) (target.bytes[target.offset + targetUpto]) + " vs termLabel=" + (char) (term.byteAt(targetUpto)) + ")"   + " arc.output=" + arc.output + " output=" + output);
+        //}
+        if (cmp != 0) {
+          break;
+        }
+        arc = arcs[1+targetUpto];
+        assert arc.label == (target.bytes[target.offset + targetUpto] & 0xFF): "arc.label=" + (char) arc.label + " targetLabel=" + (char) (target.bytes[target.offset + targetUpto] & 0xFF);
+        // TODO: we could save the outputs in local
+        // byte[][] instead of making new objs ever
+        // seek; but, often the FST doesn't have any
+        // shared bytes (but this could change if we
+        // reverse vLong byte order)
+        if (arc.output != EncryptedBlockTreeTermsReader.NO_OUTPUT) {
+          output = EncryptedBlockTreeTermsReader.FST_OUTPUTS.add(output, arc.output);
+        }
+        if (arc.isFinal()) {
+          lastFrame = stack[1+lastFrame.ord];
+        }
+        targetUpto++;
+      }
+
+
+      if (cmp == 0) {
+        final int targetUptoMid = targetUpto;
+        // Second compare the rest of the term, but
+        // don't save arc/output/frame:
+        final int targetLimit2 = Math.min(target.length, term.length());
+        while (targetUpto < targetLimit2) {
+          cmp = (term.byteAt(targetUpto)&0xFF) - (target.bytes[target.offset + targetUpto]&0xFF);
+          //if (DEBUG) {
+          //System.out.println("    cycle2 targetUpto=" + targetUpto + " (vs limit=" + targetLimit + ") cmp=" + cmp + " (targetLabel=" + (char) (target.bytes[target.offset + targetUpto]) + " vs termLabel=" + (char) (term.byteAt(targetUpto)) + ")");
+          //}
+          if (cmp != 0) {
+            break;
+          }
+          targetUpto++;
+        }
+
+        if (cmp == 0) {
+          cmp = term.length() - target.length;
+        }
+        targetUpto = targetUptoMid;
+      }
+
+      if (cmp < 0) {
+        // Common case: target term is after current
+        // term, ie, app is seeking multiple terms
+        // in sorted order
+        //if (DEBUG) {
+        //System.out.println("  target is after current (shares prefixLen=" + targetUpto + "); clear frame.scanned ord=" + lastFrame.ord);
+        //}
+        currentFrame = lastFrame;
+
+      } else if (cmp > 0) {
+        // Uncommon case: target term
+        // is before current term; this means we can
+        // keep the currentFrame but we must rewind it
+        // (so we scan from the start)
+        targetBeforeCurrentLength = 0;
+        //if (DEBUG) {
+        //System.out.println("  target is before current (shares prefixLen=" + targetUpto + "); rewind frame ord=" + lastFrame.ord);
+        //}
+        currentFrame = lastFrame;
+        currentFrame.rewind();
+      } else {
+        // Target is exactly the same as current term
+        assert term.length() == target.length;
+        if (termExists) {
+          //if (DEBUG) {
+          //System.out.println("  target is same as current; return FOUND");
+          //}
+          return SeekStatus.FOUND;
+        } else {
+          //if (DEBUG) {
+          //System.out.println("  target is same as current but term doesn't exist");
+          //}
+        }
+      }
+
+    } else {
+
+      targetBeforeCurrentLength = -1;
+      arc = fr.index.getFirstArc(arcs[0]);
+
+      // Empty string prefix must have an output (block) in the index!
+      assert arc.isFinal();
+      assert arc.output != null;
+
+      //if (DEBUG) {
+      //System.out.println("    no seek state; push root frame");
+      //}
+
+      output = arc.output;
+
+      currentFrame = staticFrame;
+
+      //term.length = 0;
+      targetUpto = 0;
+      currentFrame = pushFrame(arc, EncryptedBlockTreeTermsReader.FST_OUTPUTS.add(output, arc.nextFinalOutput), 0);
+    }
+
+    //if (DEBUG) {
+    //System.out.println("  start index loop targetUpto=" + targetUpto + " output=" + output + " currentFrame.ord+1=" + currentFrame.ord + " targetBeforeCurrentLength=" + targetBeforeCurrentLength);
+    //}
+
+    // We are done sharing the common prefix with the incoming target and where we are currently seek'd; now continue walking the index:
+    while (targetUpto < target.length) {
+
+      final int targetLabel = target.bytes[target.offset + targetUpto] & 0xFF;
+
+      final FST.Arc<BytesRef> nextArc = fr.index.findTargetArc(targetLabel, arc, getArc(1+targetUpto), fstReader);
+
+      if (nextArc == null) {
+
+        // Index is exhausted
+        // if (DEBUG) {
+        //   System.out.println("    index: index exhausted label=" + ((char) targetLabel) + " " + targetLabel);
+        // }
+            
+        validIndexPrefix = currentFrame.prefix;
+        //validIndexPrefix = targetUpto;
+
+        currentFrame.scanToFloorFrame(target);
+
+        currentFrame.loadBlock();
+
+        //if (DEBUG) System.out.println("  now scanToTerm");
+        final SeekStatus result = currentFrame.scanToTerm(target, false);
+        if (result == SeekStatus.END) {
+          term.copyBytes(target);
+          termExists = false;
+
+          if (next() != null) {
+            //if (DEBUG) {
+            //System.out.println("  return NOT_FOUND term=" + brToString(term));
+            //}
+            return SeekStatus.NOT_FOUND;
+          } else {
+            //if (DEBUG) {
+            //System.out.println("  return END");
+            //}
+            return SeekStatus.END;
+          }
+        } else {
+          //if (DEBUG) {
+          //System.out.println("  return " + result + " term=" + brToString(term));
+          //}
+          return result;
+        }
+      } else {
+        // Follow this arc
+        term.setByteAt(targetUpto, (byte) targetLabel);
+        arc = nextArc;
+        // Aggregate output as we go:
+        assert arc.output != null;
+        if (arc.output != EncryptedBlockTreeTermsReader.NO_OUTPUT) {
+          output = EncryptedBlockTreeTermsReader.FST_OUTPUTS.add(output, arc.output);
+        }
+
+        //if (DEBUG) {
+        //System.out.println("    index: follow label=" + (target.bytes[target.offset + targetUpto]&0xff) + " arc.output=" + arc.output + " arc.nfo=" + arc.nextFinalOutput);
+        //}
+        targetUpto++;
+
+        if (arc.isFinal()) {
+          //if (DEBUG) System.out.println("    arc is final!");
+          currentFrame = pushFrame(arc, EncryptedBlockTreeTermsReader.FST_OUTPUTS.add(output, arc.nextFinalOutput), targetUpto);
+          //if (DEBUG) System.out.println("    curFrame.ord=" + currentFrame.ord + " hasTerms=" + currentFrame.hasTerms);
+        }
+      }
+    }
+
+    //validIndexPrefix = targetUpto;
+    validIndexPrefix = currentFrame.prefix;
+
+    currentFrame.scanToFloorFrame(target);
+
+    currentFrame.loadBlock();
+
+    final SeekStatus result = currentFrame.scanToTerm(target, false);
+
+    if (result == SeekStatus.END) {
+      term.copyBytes(target);
+      termExists = false;
+      if (next() != null) {
+        //if (DEBUG) {
+        //System.out.println("  return NOT_FOUND term=" + term.get().utf8ToString() + " " + term);
+        //}
+        return SeekStatus.NOT_FOUND;
+      } else {
+        //if (DEBUG) {
+        //System.out.println("  return END");
+        //}
+        return SeekStatus.END;
+      }
+    } else {
+      return result;
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private void printSeekState(PrintStream out) throws IOException {
+    if (currentFrame == staticFrame) {
+      out.println("  no prior seek");
+    } else {
+      out.println("  prior seek state:");
+      int ord = 0;
+      boolean isSeekFrame = true;
+      while(true) {
+        EncryptedSegmentTermsEnumFrame f = getFrame(ord);
+        assert f != null;
+        final BytesRef prefix = new BytesRef(term.get().bytes, 0, f.prefix);
+        if (f.nextEnt == -1) {
+          out.println("    frame " + (isSeekFrame ? "(seek)" : "(next)") + " ord=" + ord + " fp=" + f.fp + (f.isFloor ? (" (fpOrig=" + f.fpOrig + ")") : "") + " prefixLen=" + f.prefix + " prefix=" + prefix + (f.nextEnt == -1 ? "" : (" (of " + f.entCount + ")")) + " hasTerms=" + f.hasTerms + " isFloor=" + f.isFloor + " code=" + ((f.fp<< EncryptedBlockTreeTermsReader.OUTPUT_FLAGS_NUM_BITS) + (f.hasTerms ? EncryptedBlockTreeTermsReader.OUTPUT_FLAG_HAS_TERMS:0) + (f.isFloor ? EncryptedBlockTreeTermsReader.OUTPUT_FLAG_IS_FLOOR:0)) + " isLastInFloor=" + f.isLastInFloor + " mdUpto=" + f.metaDataUpto + " tbOrd=" + f.getTermBlockOrd());
+        } else {
+          out.println("    frame " + (isSeekFrame ? "(seek, loaded)" : "(next, loaded)") + " ord=" + ord + " fp=" + f.fp + (f.isFloor ? (" (fpOrig=" + f.fpOrig + ")") : "") + " prefixLen=" + f.prefix + " prefix=" + prefix + " nextEnt=" + f.nextEnt + (f.nextEnt == -1 ? "" : (" (of " + f.entCount + ")")) + " hasTerms=" + f.hasTerms + " isFloor=" + f.isFloor + " code=" + ((f.fp<< EncryptedBlockTreeTermsReader.OUTPUT_FLAGS_NUM_BITS) + (f.hasTerms ? EncryptedBlockTreeTermsReader.OUTPUT_FLAG_HAS_TERMS:0) + (f.isFloor ? EncryptedBlockTreeTermsReader.OUTPUT_FLAG_IS_FLOOR:0)) + " lastSubFP=" + f.lastSubFP + " isLastInFloor=" + f.isLastInFloor + " mdUpto=" + f.metaDataUpto + " tbOrd=" + f.getTermBlockOrd());
+        }
+        if (fr.index != null) {
+          assert !isSeekFrame || f.arc != null: "isSeekFrame=" + isSeekFrame + " f.arc=" + f.arc;
+          if (f.prefix > 0 && isSeekFrame && f.arc.label != (term.byteAt(f.prefix-1)&0xFF)) {
+            out.println("      broken seek state: arc.label=" + (char) f.arc.label + " vs term byte=" + (char) (term.byteAt(f.prefix-1)&0xFF));
+            throw new RuntimeException("seek state is broken");
+          }
+          BytesRef output = Util.get(fr.index, prefix);
+          if (output == null) {
+            out.println("      broken seek state: prefix is not final in index");
+            throw new RuntimeException("seek state is broken");
+          } else if (isSeekFrame && !f.isFloor) {
+            final ByteArrayDataInput reader = new ByteArrayDataInput(output.bytes, output.offset, output.length);
+            final long codeOrig = reader.readVLong();
+            final long code = (f.fp << EncryptedBlockTreeTermsReader.OUTPUT_FLAGS_NUM_BITS) | (f.hasTerms ? EncryptedBlockTreeTermsReader.OUTPUT_FLAG_HAS_TERMS:0) | (f.isFloor ? EncryptedBlockTreeTermsReader.OUTPUT_FLAG_IS_FLOOR:0);
+            if (codeOrig != code) {
+              out.println("      broken seek state: output code=" + codeOrig + " doesn't match frame code=" + code);
+              throw new RuntimeException("seek state is broken");
+            }
+          }
+        }
+        if (f == currentFrame) {
+          break;
+        }
+        if (f.prefix == validIndexPrefix) {
+          isSeekFrame = false;
+        }
+        ord++;
+      }
+    }
+  }
+
+  /* Decodes only the term bytes of the next term.  If caller then asks for
+     metadata, ie docFreq, totalTermFreq or pulls a D/&PEnum, we then (lazily)
+     decode all metadata up to the current term. */
+  @Override
+  public BytesRef next() throws IOException {
+    if (in == null) {
+      // Fresh TermsEnum; seek to first term:
+      final FST.Arc<BytesRef> arc;
+      if (fr.index != null) {
+        arc = fr.index.getFirstArc(arcs[0]);
+        // Empty string prefix must have an output in the index!
+        assert arc.isFinal();
+      } else {
+        arc = null;
+      }
+      currentFrame = pushFrame(arc, fr.rootCode, 0);
+      currentFrame.loadBlock();
+    }
+
+    targetBeforeCurrentLength = currentFrame.ord;
+
+    assert !eof;
+    // if (DEBUG) {
+    //   System.out.println("\nBTTR.next seg=" + fr.parent.segment + " term=" + brToString(term) + " termExists?=" + termExists + " field=" + fr.fieldInfo.name + " termBlockOrd=" + currentFrame.state.termBlockOrd + " validIndexPrefix=" + validIndexPrefix);
+    //   printSeekState(System.out);
+    // }
+
+    if (currentFrame == staticFrame) {
+      // If seek was previously called and the term was
+      // cached, or seek(TermState) was called, usually
+      // caller is just going to pull a D/&PEnum or get
+      // docFreq, etc.  But, if they then call next(),
+      // this method catches up all internal state so next()
+      // works properly:
+      //if (DEBUG) System.out.println("  re-seek to pending term=" + term.utf8ToString() + " " + term);
+      final boolean result = seekExact(term.get());
+      assert result;
+    }
+
+    // Pop finished blocks
+    while (currentFrame.nextEnt == currentFrame.entCount) {
+      if (!currentFrame.isLastInFloor) {
+        // Advance to next floor block
+        currentFrame.loadNextFloorBlock();
+        break;
+      } else {
+        //if (DEBUG) System.out.println("  pop frame");
+        if (currentFrame.ord == 0) {
+          //if (DEBUG) System.out.println("  return null");
+          assert setEOF();
+          term.clear();
+          validIndexPrefix = 0;
+          currentFrame.rewind();
+          termExists = false;
+          return null;
+        }
+        final long lastFP = currentFrame.fpOrig;
+        currentFrame = stack[currentFrame.ord-1];
+
+        if (currentFrame.nextEnt == -1 || currentFrame.lastSubFP != lastFP) {
+          // We popped into a frame that's not loaded
+          // yet or not scan'd to the right entry
+          currentFrame.scanToFloorFrame(term.get());
+          currentFrame.loadBlock();
+          currentFrame.scanToSubBlock(lastFP);
+        }
+
+        // Note that the seek state (last seek) has been
+        // invalidated beyond this depth
+        validIndexPrefix = Math.min(validIndexPrefix, currentFrame.prefix);
+        //if (DEBUG) {
+        //System.out.println("  reset validIndexPrefix=" + validIndexPrefix);
+        //}
+      }
+    }
+
+    while(true) {
+      if (currentFrame.next()) {
+        // Push to new block:
+        //if (DEBUG) System.out.println("  push frame");
+        currentFrame = pushFrame(null, currentFrame.lastSubFP, term.length());
+        // This is a "next" frame -- even if it's
+        // floor'd we must pretend it isn't so we don't
+        // try to scan to the right floor frame:
+        currentFrame.loadBlock();
+      } else {
+        //if (DEBUG) System.out.println("  return term=" + brToString(term) + " currentFrame.ord=" + currentFrame.ord);
+        return term.get();
+      }
+    }
+  }
+
+  @Override
+  public BytesRef term() {
+    assert !eof;
+    return term.get();
+  }
+
+  @Override
+  public int docFreq() throws IOException {
+    assert !eof;
+    //if (DEBUG) System.out.println("BTR.docFreq");
+    currentFrame.decodeMetaData();
+    //if (DEBUG) System.out.println("  return " + currentFrame.state.docFreq);
+    return currentFrame.state.docFreq;
+  }
+
+  @Override
+  public long totalTermFreq() throws IOException {
+    assert !eof;
+    currentFrame.decodeMetaData();
+    return currentFrame.state.totalTermFreq;
+  }
+
+  @Override
+  public PostingsEnum postings(PostingsEnum reuse, int flags) throws IOException {
+    assert !eof;
+    //if (DEBUG) {
+    //System.out.println("BTTR.docs seg=" + segment);
+    //}
+    currentFrame.decodeMetaData();
+    //if (DEBUG) {
+    //System.out.println("  state=" + currentFrame.state);
+    //}
+    return fr.parent.postingsReader.postings(fr.fieldInfo, currentFrame.state, reuse, flags);
+  }
+
+  @Override
+  public void seekExact(BytesRef target, TermState otherState) {
+    // if (DEBUG) {
+    //   System.out.println("BTTR.seekExact termState seg=" + segment + " target=" + target.utf8ToString() + " " + target + " state=" + otherState);
+    // }
+    assert clearEOF();
+    if (target.compareTo(term.get()) != 0 || !termExists) {
+      assert otherState != null && otherState instanceof BlockTermState;
+      currentFrame = staticFrame;
+      currentFrame.state.copyFrom(otherState);
+      term.copyBytes(target);
+      currentFrame.metaDataUpto = currentFrame.getTermBlockOrd();
+      assert currentFrame.metaDataUpto > 0;
+      validIndexPrefix = 0;
+    } else {
+      // if (DEBUG) {
+      //   System.out.println("  skip seek: already on target state=" + currentFrame.state);
+      // }
+    }
+  }
+      
+  @Override
+  public TermState termState() throws IOException {
+    assert !eof;
+    currentFrame.decodeMetaData();
+    TermState ts = currentFrame.state.clone();
+    //if (DEBUG) System.out.println("BTTR.termState seg=" + segment + " state=" + ts);
+    return ts;
+  }
+
+  @Override
+  public void seekExact(long ord) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public long ord() {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedSegmentTermsEnumFrame.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedSegmentTermsEnumFrame.java
new file mode 100644
index 0000000..a9abe5a
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/EncryptedSegmentTermsEnumFrame.java
@@ -0,0 +1,840 @@
+package org.apache.lucene.codecs.encrypted.blocktree;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.BlockTermState;
+import org.apache.lucene.index.IndexOptions;
+import org.apache.lucene.index.TermsEnum.SeekStatus;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.fst.FST;
+
+import java.io.IOException;
+
+final class EncryptedSegmentTermsEnumFrame {
+  // Our index in stack[]:
+  final int ord;
+
+  boolean hasTerms;
+  boolean hasTermsOrig;
+  boolean isFloor;
+
+  FST.Arc<BytesRef> arc;
+
+  final boolean versionAutoPrefix;
+
+  //static boolean DEBUG = BlockTreeTermsWriter.DEBUG;
+
+  // File pointer where this block was loaded from
+  long fp;
+  long fpOrig;
+  long fpEnd;
+
+  byte[] suffixBytes = new byte[128];
+  final ByteArrayDataInput suffixesReader = new ByteArrayDataInput();
+
+  byte[] statBytes = new byte[64];
+  final ByteArrayDataInput statsReader = new ByteArrayDataInput();
+
+  byte[] floorData = new byte[32];
+  final ByteArrayDataInput floorDataReader = new ByteArrayDataInput();
+
+  // Length of prefix shared by all terms in this block
+  int prefix;
+
+  // Number of entries (term or sub-block) in this block
+  int entCount;
+
+  // Which term we will next read, or -1 if the block
+  // isn't loaded yet
+  int nextEnt;
+
+  // True if this block is either not a floor block,
+  // or, it's the last sub-block of a floor block
+  boolean isLastInFloor;
+
+  // True if all entries are terms
+  boolean isLeafBlock;
+
+  long lastSubFP;
+
+  int nextFloorLabel;
+  int numFollowFloorBlocks;
+
+  // Next term to decode metaData; we decode metaData
+  // lazily so that scanning to find the matching term is
+  // fast and only if you find a match and app wants the
+  // stats or docs/positions enums, will we decode the
+  // metaData
+  int metaDataUpto;
+
+  final BlockTermState state;
+
+  // metadata buffer, holding monotonic values
+  final long[] longs;
+  // metadata buffer, holding general values
+  byte[] bytes = new byte[32];
+  final ByteArrayDataInput bytesReader = new ByteArrayDataInput();
+
+  private final EncryptedSegmentTermsEnum ste;
+
+  public EncryptedSegmentTermsEnumFrame(EncryptedSegmentTermsEnum ste, int ord) throws IOException {
+    this.ste = ste;
+    this.ord = ord;
+    this.state = ste.fr.parent.postingsReader.newTermState();
+    this.state.totalTermFreq = -1;
+    this.longs = new long[ste.fr.longsSize];
+    this.versionAutoPrefix = ste.fr.parent.anyAutoPrefixTerms;
+  }
+
+  public void setFloorData(ByteArrayDataInput in, BytesRef source) {
+    final int numBytes = source.length - (in.getPosition() - source.offset);
+    if (numBytes > floorData.length) {
+      floorData = new byte[ArrayUtil.oversize(numBytes, 1)];
+    }
+    System.arraycopy(source.bytes, source.offset+in.getPosition(), floorData, 0, numBytes);
+    floorDataReader.reset(floorData, 0, numBytes);
+    numFollowFloorBlocks = floorDataReader.readVInt();
+    nextFloorLabel = floorDataReader.readByte() & 0xff;
+    //if (DEBUG) {
+    //System.out.println("    setFloorData fpOrig=" + fpOrig + " bytes=" + new BytesRef(source.bytes, source.offset + in.getPosition(), numBytes) + " numFollowFloorBlocks=" + numFollowFloorBlocks + " nextFloorLabel=" + toHex(nextFloorLabel));
+    //}
+  }
+
+  public int getTermBlockOrd() {
+    return isLeafBlock ? nextEnt : state.termBlockOrd;
+  }
+
+  void loadNextFloorBlock() throws IOException {
+    //if (DEBUG) {
+    //System.out.println("    loadNextFloorBlock fp=" + fp + " fpEnd=" + fpEnd);
+    //}
+    assert arc == null || isFloor: "arc=" + arc + " isFloor=" + isFloor;
+    fp = fpEnd;
+    nextEnt = -1;
+    loadBlock();
+  }
+
+  /* Does initial decode of next block of terms; this
+     doesn't actually decode the docFreq, totalTermFreq,
+     postings details (frq/prx offset, etc.) metadata;
+     it just loads them as byte[] blobs which are then      
+     decoded on-demand if the metadata is ever requested
+     for any term in this block.  This enables terms-only
+     intensive consumes (eg certain MTQs, respelling) to
+     not pay the price of decoding metadata they won't
+     use. */
+  void loadBlock() throws IOException {
+
+    // Clone the IndexInput lazily, so that consumers
+    // that just pull a TermsEnum to
+    // seekExact(TermState) don't pay this cost:
+    ste.initIndexInput();
+
+    if (nextEnt != -1) {
+      // Already loaded
+      return;
+    }
+    //System.out.println("blc=" + blockLoadCount);
+
+    ste.in.seek(fp);
+    int code = ste.in.readVInt();
+    entCount = code >>> 1;
+    assert entCount > 0;
+    isLastInFloor = (code & 1) != 0;
+
+    assert arc == null || (isLastInFloor || isFloor): "fp=" + fp + " arc=" + arc + " isFloor=" + isFloor + " isLastInFloor=" + isLastInFloor;
+
+    // TODO: if suffixes were stored in random-access
+    // array structure, then we could do binary search
+    // instead of linear scan to find target term; eg
+    // we could have simple array of offsets
+
+    // term suffixes:
+    code = ste.in.readVInt();
+    isLeafBlock = (code & 1) != 0;
+    int numBytes = code >>> 1; // decode size of encrypted suffixes byte[]
+    if (suffixBytes.length < numBytes) {
+      suffixBytes = new byte[ArrayUtil.oversize(numBytes, 1)];
+    }
+    ste.in.readBytes(suffixBytes, 0, numBytes); // read encrypted suffixes byte[]
+    int read = 0;
+    try {
+      // we can reuse suffixBytes as output since the decrypt method is copy-safe
+      read = ste.decipher.doFinal(suffixBytes, 0, numBytes, suffixBytes, 0);
+    } catch (Exception e) {
+      throw new IOException("Deciphering of term suffix block failed", e);
+    }
+    suffixesReader.reset(suffixBytes, 0, read);
+
+    /*if (DEBUG) {
+      if (arc == null) {
+      System.out.println("    loadBlock (next) fp=" + fp + " entCount=" + entCount + " prefixLen=" + prefix + " isLastInFloor=" + isLastInFloor + " leaf?=" + isLeafBlock);
+      } else {
+      System.out.println("    loadBlock (seek) fp=" + fp + " entCount=" + entCount + " prefixLen=" + prefix + " hasTerms?=" + hasTerms + " isFloor?=" + isFloor + " isLastInFloor=" + isLastInFloor + " leaf?=" + isLeafBlock);
+      }
+      }*/
+
+    // stats
+    numBytes = ste.in.readVInt();
+    if (statBytes.length < numBytes) {
+      statBytes = new byte[ArrayUtil.oversize(numBytes, 1)];
+    }
+    ste.in.readBytes(statBytes, 0, numBytes);
+    statsReader.reset(statBytes, 0, numBytes);
+    metaDataUpto = 0;
+
+    state.termBlockOrd = 0;
+    nextEnt = 0;
+    lastSubFP = -1;
+
+    // TODO: we could skip this if !hasTerms; but
+    // that's rare so won't help much
+    // metadata
+    numBytes = ste.in.readVInt();
+    if (bytes.length < numBytes) {
+      bytes = new byte[ArrayUtil.oversize(numBytes, 1)];
+    }
+    ste.in.readBytes(bytes, 0, numBytes);
+    bytesReader.reset(bytes, 0, numBytes);
+
+    // Sub-blocks of a single floor block are always
+    // written one after another -- tail recurse:
+    fpEnd = ste.in.getFilePointer();
+    // if (DEBUG) {
+    //   System.out.println("      fpEnd=" + fpEnd);
+    // }
+  }
+
+  void rewind() {
+
+    // Force reload:
+    fp = fpOrig;
+    nextEnt = -1;
+    hasTerms = hasTermsOrig;
+    if (isFloor) {
+      floorDataReader.rewind();
+      numFollowFloorBlocks = floorDataReader.readVInt();
+      assert numFollowFloorBlocks > 0;
+      nextFloorLabel = floorDataReader.readByte() & 0xff;
+    }
+
+    /*
+    //System.out.println("rewind");
+    // Keeps the block loaded, but rewinds its state:
+    if (nextEnt > 0 || fp != fpOrig) {
+    if (DEBUG) {
+    System.out.println("      rewind frame ord=" + ord + " fpOrig=" + fpOrig + " fp=" + fp + " hasTerms?=" + hasTerms + " isFloor?=" + isFloor + " nextEnt=" + nextEnt + " prefixLen=" + prefix);
+    }
+    if (fp != fpOrig) {
+    fp = fpOrig;
+    nextEnt = -1;
+    } else {
+    nextEnt = 0;
+    }
+    hasTerms = hasTermsOrig;
+    if (isFloor) {
+    floorDataReader.rewind();
+    numFollowFloorBlocks = floorDataReader.readVInt();
+    nextFloorLabel = floorDataReader.readByte() & 0xff;
+    }
+    assert suffixBytes != null;
+    suffixesReader.rewind();
+    assert statBytes != null;
+    statsReader.rewind();
+    metaDataUpto = 0;
+    state.termBlockOrd = 0;
+    // TODO: skip this if !hasTerms?  Then postings
+    // impl wouldn't have to write useless 0 byte
+    postingsReader.resetTermsBlock(fieldInfo, state);
+    lastSubFP = -1;
+    } else if (DEBUG) {
+    System.out.println("      skip rewind fp=" + fp + " fpOrig=" + fpOrig + " nextEnt=" + nextEnt + " ord=" + ord);
+    }
+    */
+  }
+
+  // Decodes next entry; returns true if it's a sub-block
+  public boolean next() throws IOException {
+    if (isLeafBlock) {
+      nextLeaf();
+      return false;
+    } else {
+      return nextNonLeaf();
+    }
+  }
+
+  public void nextLeaf() {
+    //if (DEBUG) System.out.println("  frame.next ord=" + ord + " nextEnt=" + nextEnt + " entCount=" + entCount);
+    assert nextEnt != -1 && nextEnt < entCount: "nextEnt=" + nextEnt + " entCount=" + entCount + " fp=" + fp;
+    nextEnt++;
+    suffix = suffixesReader.readVInt();
+    startBytePos = suffixesReader.getPosition();
+    ste.term.setLength(prefix + suffix);
+    ste.term.grow(ste.term.length());
+    suffixesReader.readBytes(ste.term.bytes(), prefix, suffix);
+    ste.termExists = true;
+  }
+
+  public boolean nextNonLeaf() throws IOException {
+    //if (DEBUG) System.out.println("  stef.next ord=" + ord + " nextEnt=" + nextEnt + " entCount=" + entCount + " fp=" + suffixesReader.getPosition());
+    while (true) {
+      if (nextEnt == entCount) {
+        assert arc == null || (isFloor && isLastInFloor == false): "isFloor=" + isFloor + " isLastInFloor=" + isLastInFloor;
+        loadNextFloorBlock();
+        if (isLeafBlock) {
+          nextLeaf();
+          return false;
+        } else {
+          continue;
+        }
+      }
+        
+      assert nextEnt != -1 && nextEnt < entCount: "nextEnt=" + nextEnt + " entCount=" + entCount + " fp=" + fp;
+      nextEnt++;
+      final int code = suffixesReader.readVInt();
+      if (versionAutoPrefix == false) {
+        suffix = code >>> 1;
+        startBytePos = suffixesReader.getPosition();
+        ste.term.setLength(prefix + suffix);
+        ste.term.grow(ste.term.length());
+        suffixesReader.readBytes(ste.term.bytes(), prefix, suffix);
+        if ((code & 1) == 0) {
+          // A normal term
+          ste.termExists = true;
+          subCode = 0;
+          state.termBlockOrd++;
+          return false;
+        } else {
+          // A sub-block; make sub-FP absolute:
+          ste.termExists = false;
+          subCode = suffixesReader.readVLong();
+          lastSubFP = fp - subCode;
+          //if (DEBUG) {
+          //System.out.println("    lastSubFP=" + lastSubFP);
+          //}
+          return true;
+        }
+      } else {
+        suffix = code >>> 2;
+        startBytePos = suffixesReader.getPosition();
+        ste.term.setLength(prefix + suffix);
+        ste.term.grow(ste.term.length());
+        suffixesReader.readBytes(ste.term.bytes(), prefix, suffix);
+
+        switch(code & 3) {
+        case 0:
+          // A normal term
+          ste.termExists = true;
+          subCode = 0;
+          state.termBlockOrd++;
+          return false;
+        case 1:
+          // A sub-block; make sub-FP absolute:
+          ste.termExists = false;
+          subCode = suffixesReader.readVLong();
+          lastSubFP = fp - subCode;
+          //if (DEBUG) {
+          //System.out.println("    lastSubFP=" + lastSubFP);
+          //}
+          return true;
+        case 2:
+        case 3:
+          // A prefix term: skip it
+          state.termBlockOrd++;
+          suffixesReader.readByte();
+          continue;
+        }
+      }
+    }
+  }
+        
+  // TODO: make this array'd so we can do bin search?
+  // likely not worth it?  need to measure how many
+  // floor blocks we "typically" get
+  public void scanToFloorFrame(BytesRef target) {
+
+    if (!isFloor || target.length <= prefix) {
+      // if (DEBUG) {
+      //   System.out.println("    scanToFloorFrame skip: isFloor=" + isFloor + " target.length=" + target.length + " vs prefix=" + prefix);
+      // }
+      return;
+    }
+
+    final int targetLabel = target.bytes[target.offset + prefix] & 0xFF;
+
+    // if (DEBUG) {
+    //   System.out.println("    scanToFloorFrame fpOrig=" + fpOrig + " targetLabel=" + toHex(targetLabel) + " vs nextFloorLabel=" + toHex(nextFloorLabel) + " numFollowFloorBlocks=" + numFollowFloorBlocks);
+    // }
+
+    if (targetLabel < nextFloorLabel) {
+      // if (DEBUG) {
+      //   System.out.println("      already on correct block");
+      // }
+      return;
+    }
+
+    assert numFollowFloorBlocks != 0;
+
+    long newFP = fpOrig;
+    while (true) {
+      final long code = floorDataReader.readVLong();
+      newFP = fpOrig + (code >>> 1);
+      hasTerms = (code & 1) != 0;
+      // if (DEBUG) {
+      //   System.out.println("      label=" + toHex(nextFloorLabel) + " fp=" + newFP + " hasTerms?=" + hasTerms + " numFollowFloor=" + numFollowFloorBlocks);
+      // }
+            
+      isLastInFloor = numFollowFloorBlocks == 1;
+      numFollowFloorBlocks--;
+
+      if (isLastInFloor) {
+        nextFloorLabel = 256;
+        // if (DEBUG) {
+        //   System.out.println("        stop!  last block nextFloorLabel=" + toHex(nextFloorLabel));
+        // }
+        break;
+      } else {
+        nextFloorLabel = floorDataReader.readByte() & 0xff;
+        if (targetLabel < nextFloorLabel) {
+          // if (DEBUG) {
+          //   System.out.println("        stop!  nextFloorLabel=" + toHex(nextFloorLabel));
+          // }
+          break;
+        }
+      }
+    }
+
+    if (newFP != fp) {
+      // Force re-load of the block:
+      // if (DEBUG) {
+      //   System.out.println("      force switch to fp=" + newFP + " oldFP=" + fp);
+      // }
+      nextEnt = -1;
+      fp = newFP;
+    } else {
+      // if (DEBUG) {
+      //   System.out.println("      stay on same fp=" + newFP);
+      // }
+    }
+  }
+    
+  public void decodeMetaData() throws IOException {
+
+    //if (DEBUG) System.out.println("\nBTTR.decodeMetadata seg=" + segment + " mdUpto=" + metaDataUpto + " vs termBlockOrd=" + state.termBlockOrd);
+
+    // lazily catch up on metadata decode:
+    final int limit = getTermBlockOrd();
+    boolean absolute = metaDataUpto == 0;
+    assert limit > 0;
+
+    // TODO: better API would be "jump straight to term=N"???
+    while (metaDataUpto < limit) {
+
+      // TODO: we could make "tiers" of metadata, ie,
+      // decode docFreq/totalTF but don't decode postings
+      // metadata; this way caller could get
+      // docFreq/totalTF w/o paying decode cost for
+      // postings
+
+      // TODO: if docFreq were bulk decoded we could
+      // just skipN here:
+
+      // stats
+      state.docFreq = statsReader.readVInt();
+      //if (DEBUG) System.out.println("    dF=" + state.docFreq);
+      if (ste.fr.fieldInfo.getIndexOptions() != IndexOptions.DOCS) {
+        state.totalTermFreq = state.docFreq + statsReader.readVLong();
+        //if (DEBUG) System.out.println("    totTF=" + state.totalTermFreq);
+      }
+      // metadata 
+      for (int i = 0; i < ste.fr.longsSize; i++) {
+        longs[i] = bytesReader.readVLong();
+      }
+      ste.fr.parent.postingsReader.decodeTerm(longs, bytesReader, ste.fr.fieldInfo, state, absolute);
+
+      metaDataUpto++;
+      absolute = false;
+    }
+    state.termBlockOrd = metaDataUpto;
+  }
+
+  // Used only by assert
+  private boolean prefixMatches(BytesRef target) {
+    for(int bytePos=0;bytePos<prefix;bytePos++) {
+      if (target.bytes[target.offset + bytePos] != ste.term.byteAt(bytePos)) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  // Scans to sub-block that has this target fp; only
+  // called by next(); NOTE: does not set
+  // startBytePos/suffix as a side effect
+  public void scanToSubBlock(long subFP) {
+    assert !isLeafBlock;
+    //if (DEBUG) System.out.println("  scanToSubBlock fp=" + fp + " subFP=" + subFP + " entCount=" + entCount + " lastSubFP=" + lastSubFP);
+    //assert nextEnt == 0;
+    if (lastSubFP == subFP) {
+      //if (DEBUG) System.out.println("    already positioned");
+      return;
+    }
+    assert subFP < fp : "fp=" + fp + " subFP=" + subFP;
+    final long targetSubCode = fp - subFP;
+    //if (DEBUG) System.out.println("    targetSubCode=" + targetSubCode);
+    while(true) {
+      assert nextEnt < entCount;
+      nextEnt++;
+      final int code = suffixesReader.readVInt();
+      if (versionAutoPrefix == false) {
+        suffixesReader.skipBytes(code >>> 1);
+        if ((code & 1) != 0) {
+          final long subCode = suffixesReader.readVLong();
+          if (targetSubCode == subCode) {
+            //if (DEBUG) System.out.println("        match!");
+            lastSubFP = subFP;
+            return;
+          }
+        } else {
+          state.termBlockOrd++;
+        }
+      } else {
+        int flag = code & 3;
+        suffixesReader.skipBytes(code >>> 2);
+        //if (DEBUG) System.out.println("    " + nextEnt + " (of " + entCount + ") ent isSubBlock=" + ((code&1)==1));
+        if (flag == 1) {
+          // Sub-block
+          final long subCode = suffixesReader.readVLong();
+          //if (DEBUG) System.out.println("      subCode=" + subCode);
+          if (targetSubCode == subCode) {
+            //if (DEBUG) System.out.println("        match!");
+            lastSubFP = subFP;
+            return;
+          }
+        } else {
+          state.termBlockOrd++;
+          if (flag == 2 || flag == 3) {
+            // Floor'd prefix term
+            suffixesReader.readByte();
+          }
+        }
+      }
+    }
+  }
+
+  // NOTE: sets startBytePos/suffix as a side effect
+  public SeekStatus scanToTerm(BytesRef target, boolean exactOnly) throws IOException {
+    return isLeafBlock ? scanToTermLeaf(target, exactOnly) : scanToTermNonLeaf(target, exactOnly);
+  }
+
+  private int startBytePos;
+  private int suffix;
+  private long subCode;
+
+  // for debugging
+  /*
+  @SuppressWarnings("unused")
+  static String brToString(BytesRef b) {
+    try {
+      return b.utf8ToString() + " " + b;
+    } catch (Throwable t) {
+      // If BytesRef isn't actually UTF8, or it's eg a
+      // prefix of UTF8 that ends mid-unicode-char, we
+      // fallback to hex:
+      return b.toString();
+    }
+  }
+  */
+
+  // Target's prefix matches this block's prefix; we
+  // scan the entries check if the suffix matches.
+  public SeekStatus scanToTermLeaf(BytesRef target, boolean exactOnly) throws IOException {
+
+    // if (DEBUG) System.out.println("    scanToTermLeaf: block fp=" + fp + " prefix=" + prefix + " nextEnt=" + nextEnt + " (of " + entCount + ") target=" + brToString(target) + " term=" + brToString(term));
+
+    assert nextEnt != -1;
+
+    ste.termExists = true;
+    subCode = 0;
+
+    if (nextEnt == entCount) {
+      if (exactOnly) {
+        fillTerm();
+      }
+      return SeekStatus.END;
+    }
+
+    assert prefixMatches(target);
+
+    // Loop over each entry (term or sub-block) in this block:
+    //nextTerm: while(nextEnt < entCount) {
+    nextTerm: while (true) {
+      nextEnt++;
+
+      suffix = suffixesReader.readVInt();
+
+      // if (DEBUG) {
+      //   BytesRef suffixBytesRef = new BytesRef();
+      //   suffixBytesRef.bytes = suffixBytes;
+      //   suffixBytesRef.offset = suffixesReader.getPosition();
+      //   suffixBytesRef.length = suffix;
+      //   System.out.println("      cycle: term " + (nextEnt-1) + " (of " + entCount + ") suffix=" + brToString(suffixBytesRef));
+      // }
+
+      final int termLen = prefix + suffix;
+      startBytePos = suffixesReader.getPosition();
+      suffixesReader.skipBytes(suffix);
+
+      final int targetLimit = target.offset + (target.length < termLen ? target.length : termLen);
+      int targetPos = target.offset + prefix;
+
+      // Loop over bytes in the suffix, comparing to
+      // the target
+      int bytePos = startBytePos;
+      while(true) {
+        final int cmp;
+        final boolean stop;
+        if (targetPos < targetLimit) {
+          cmp = (suffixBytes[bytePos++]&0xFF) - (target.bytes[targetPos++]&0xFF);
+          stop = false;
+        } else {
+          assert targetPos == targetLimit;
+          cmp = termLen - target.length;
+          stop = true;
+        }
+
+        if (cmp < 0) {
+          // Current entry is still before the target;
+          // keep scanning
+
+          if (nextEnt == entCount) {
+            // We are done scanning this block
+            break nextTerm;
+          } else {
+            continue nextTerm;
+          }
+        } else if (cmp > 0) {
+
+          // Done!  Current entry is after target --
+          // return NOT_FOUND:
+          fillTerm();
+
+          //if (DEBUG) System.out.println("        not found");
+          return SeekStatus.NOT_FOUND;
+        } else if (stop) {
+          // Exact match!
+
+          // This cannot be a sub-block because we
+          // would have followed the index to this
+          // sub-block from the start:
+
+          assert ste.termExists;
+          fillTerm();
+          //if (DEBUG) System.out.println("        found!");
+          return SeekStatus.FOUND;
+        }
+      }
+    }
+
+    // It is possible (and OK) that terms index pointed us
+    // at this block, but, we scanned the entire block and
+    // did not find the term to position to.  This happens
+    // when the target is after the last term in the block
+    // (but, before the next term in the index).  EG
+    // target could be foozzz, and terms index pointed us
+    // to the foo* block, but the last term in this block
+    // was fooz (and, eg, first term in the next block will
+    // bee fop).
+    //if (DEBUG) System.out.println("      block end");
+    if (exactOnly) {
+      fillTerm();
+    }
+
+    // TODO: not consistent that in the
+    // not-exact case we don't next() into the next
+    // frame here
+    return SeekStatus.END;
+  }
+
+  // Target's prefix matches this block's prefix; we
+  // scan the entries check if the suffix matches.
+  public SeekStatus scanToTermNonLeaf(BytesRef target, boolean exactOnly) throws IOException {
+
+    //if (DEBUG) System.out.println("    scanToTermNonLeaf: block fp=" + fp + " prefix=" + prefix + " nextEnt=" + nextEnt + " (of " + entCount + ") target=" + brToString(target) + " term=" + brToString(target));
+
+    assert nextEnt != -1;
+
+    if (nextEnt == entCount) {
+      if (exactOnly) {
+        fillTerm();
+        ste.termExists = subCode == 0;
+      }
+      return SeekStatus.END;
+    }
+
+    assert prefixMatches(target);
+
+    // Loop over each entry (term or sub-block) in this block:
+    nextTerm: while(nextEnt < entCount) {
+
+      nextEnt++;
+
+      final int code = suffixesReader.readVInt();
+      if (versionAutoPrefix == false) {
+        suffix = code >>> 1;
+      } else {
+        suffix = code >>> 2;
+      }
+
+      //if (DEBUG) {
+      //  BytesRef suffixBytesRef = new BytesRef();
+      //  suffixBytesRef.bytes = suffixBytes;
+      //  suffixBytesRef.offset = suffixesReader.getPosition();
+      //  suffixBytesRef.length = suffix;
+      //  System.out.println("      cycle: " + ((code&1)==1 ? "sub-block" : "term") + " " + (nextEnt-1) + " (of " + entCount + ") suffix=" + brToString(suffixBytesRef));
+      //}
+
+      final int termLen = prefix + suffix;
+      startBytePos = suffixesReader.getPosition();
+      suffixesReader.skipBytes(suffix);
+      if (versionAutoPrefix == false) {
+        ste.termExists = (code & 1) == 0;
+        if (ste.termExists) {
+          state.termBlockOrd++;
+          subCode = 0;
+        } else {
+          subCode = suffixesReader.readVLong();
+          lastSubFP = fp - subCode;
+        }
+      } else {
+        switch (code & 3) {
+        case 0:
+          // Normal term
+          ste.termExists = true;
+          state.termBlockOrd++;
+          subCode = 0;
+          break;
+        case 1:
+          // Sub-block
+          ste.termExists = false;
+          subCode = suffixesReader.readVLong();
+          lastSubFP = fp - subCode;
+          break;
+        case 2:
+        case 3:
+          // Floor prefix term: skip it
+          //if (DEBUG) System.out.println("        skip floor prefix term");
+          suffixesReader.readByte();
+          ste.termExists = false;
+          state.termBlockOrd++;
+          continue;
+        }
+      }
+
+      final int targetLimit = target.offset + (target.length < termLen ? target.length : termLen);
+      int targetPos = target.offset + prefix;
+
+      // Loop over bytes in the suffix, comparing to
+      // the target
+      int bytePos = startBytePos;
+      while (true) {
+        final int cmp;
+        final boolean stop;
+        if (targetPos < targetLimit) {
+          cmp = (suffixBytes[bytePos++]&0xFF) - (target.bytes[targetPos++]&0xFF);
+          stop = false;
+        } else {
+          assert targetPos == targetLimit;
+          cmp = termLen - target.length;
+          stop = true;
+        }
+
+        if (cmp < 0) {
+          // Current entry is still before the target;
+          // keep scanning
+          continue nextTerm;
+        } else if (cmp > 0) {
+
+          // Done!  Current entry is after target --
+          // return NOT_FOUND:
+          fillTerm();
+
+          //if (DEBUG) System.out.println("        maybe done exactOnly=" + exactOnly + " ste.termExists=" + ste.termExists);
+
+          if (!exactOnly && !ste.termExists) {
+            //System.out.println("  now pushFrame");
+            // TODO this 
+            // We are on a sub-block, and caller wants
+            // us to position to the next term after
+            // the target, so we must recurse into the
+            // sub-frame(s):
+            ste.currentFrame = ste.pushFrame(null, ste.currentFrame.lastSubFP, termLen);
+            ste.currentFrame.loadBlock();
+            while (ste.currentFrame.next()) {
+              ste.currentFrame = ste.pushFrame(null, ste.currentFrame.lastSubFP, ste.term.length());
+              ste.currentFrame.loadBlock();
+            }
+          }
+                
+          //if (DEBUG) System.out.println("        not found");
+          return SeekStatus.NOT_FOUND;
+        } else if (stop) {
+          // Exact match!
+
+          // This cannot be a sub-block because we
+          // would have followed the index to this
+          // sub-block from the start:
+
+          assert ste.termExists;
+          fillTerm();
+          //if (DEBUG) System.out.println("        found!");
+          return SeekStatus.FOUND;
+        }
+      }
+    }
+
+    // It is possible (and OK) that terms index pointed us
+    // at this block, but, we scanned the entire block and
+    // did not find the term to position to.  This happens
+    // when the target is after the last term in the block
+    // (but, before the next term in the index).  EG
+    // target could be foozzz, and terms index pointed us
+    // to the foo* block, but the last term in this block
+    // was fooz (and, eg, first term in the next block will
+    // bee fop).
+    //if (DEBUG) System.out.println("      block end");
+    if (exactOnly) {
+      fillTerm();
+    }
+
+    // TODO: not consistent that in the
+    // not-exact case we don't next() into the next
+    // frame here
+    return SeekStatus.END;
+  }
+
+  private void fillTerm() {
+    final int termLength = prefix + suffix;
+    ste.term.setLength(termLength);
+    ste.term.grow(termLength);
+    System.arraycopy(suffixBytes, startBytePos, ste.term.bytes(), prefix, suffix);
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/Stats.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/Stats.java
new file mode 100644
index 0000000..8bb3b92
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/blocktree/Stats.java
@@ -0,0 +1,195 @@
+package org.apache.lucene.codecs.encrypted.blocktree;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.PostingsReaderBase;
+import org.apache.lucene.codecs.blocktree.FieldReader;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.IOUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Locale;
+
+/**
+ * BlockTree statistics for a single field 
+ * returned by {@link FieldReader#getStats()}.
+ * @lucene.internal
+ */
+public class Stats {
+  /** Byte size of the index. */
+  public long indexNumBytes;
+
+  /** Total number of terms in the field. */
+  public long totalTermCount;
+
+  /** Total number of bytes (sum of term lengths) across all terms in the field. */
+  public long totalTermBytes;
+
+  // TODO: add total auto-prefix term count
+
+  /** The number of normal (non-floor) blocks in the terms file. */
+  public int nonFloorBlockCount;
+
+  /** The number of floor blocks (meta-blocks larger than the
+   *  allowed {@code maxItemsPerBlock}) in the terms file. */
+  public int floorBlockCount;
+    
+  /** The number of sub-blocks within the floor blocks. */
+  public int floorSubBlockCount;
+
+  /** The number of "internal" blocks (that have both
+   *  terms and sub-blocks). */
+  public int mixedBlockCount;
+
+  /** The number of "leaf" blocks (blocks that have only
+   *  terms). */
+  public int termsOnlyBlockCount;
+
+  /** The number of "internal" blocks that do not contain
+   *  terms (have only sub-blocks). */
+  public int subBlocksOnlyBlockCount;
+
+  /** Total number of blocks. */
+  public int totalBlockCount;
+
+  /** Number of blocks at each prefix depth. */
+  public int[] blockCountByPrefixLen = new int[10];
+  private int startBlockCount;
+  private int endBlockCount;
+
+  /** Total number of bytes used to store term suffixes. */
+  public long totalBlockSuffixBytes;
+
+  /** Total number of bytes used to store term stats (not
+   *  including what the {@link PostingsReaderBase}
+   *  stores. */
+  public long totalBlockStatsBytes;
+
+  /** Total bytes stored by the {@link PostingsReaderBase},
+   *  plus the other few vInts stored in the frame. */
+  public long totalBlockOtherBytes;
+
+  /** Segment name. */
+  public final String segment;
+
+  /** Field name. */
+  public final String field;
+
+  Stats(String segment, String field) {
+    this.segment = segment;
+    this.field = field;
+  }
+
+  void startBlock(EncryptedSegmentTermsEnumFrame frame, boolean isFloor) {
+    totalBlockCount++;
+    if (isFloor) {
+      if (frame.fp == frame.fpOrig) {
+        floorBlockCount++;
+      }
+      floorSubBlockCount++;
+    } else {
+      nonFloorBlockCount++;
+    }
+
+    if (blockCountByPrefixLen.length <= frame.prefix) {
+      blockCountByPrefixLen = ArrayUtil.grow(blockCountByPrefixLen, 1+frame.prefix);
+    }
+    blockCountByPrefixLen[frame.prefix]++;
+    startBlockCount++;
+    totalBlockSuffixBytes += frame.suffixesReader.length();
+    totalBlockStatsBytes += frame.statsReader.length();
+  }
+
+  void endBlock(EncryptedSegmentTermsEnumFrame frame) {
+    final int termCount = frame.isLeafBlock ? frame.entCount : frame.state.termBlockOrd;
+    final int subBlockCount = frame.entCount - termCount;
+    totalTermCount += termCount;
+    if (termCount != 0 && subBlockCount != 0) {
+      mixedBlockCount++;
+    } else if (termCount != 0) {
+      termsOnlyBlockCount++;
+    } else if (subBlockCount != 0) {
+      subBlocksOnlyBlockCount++;
+    } else {
+      throw new IllegalStateException();
+    }
+    endBlockCount++;
+    final long otherBytes = frame.fpEnd - frame.fp - frame.suffixesReader.length() - frame.statsReader.length();
+    assert otherBytes > 0 : "otherBytes=" + otherBytes + " frame.fp=" + frame.fp + " frame.fpEnd=" + frame.fpEnd;
+    totalBlockOtherBytes += otherBytes;
+  }
+
+  void term(BytesRef term) {
+    totalTermBytes += term.length;
+  }
+
+  void finish() {
+    assert startBlockCount == endBlockCount: "startBlockCount=" + startBlockCount + " endBlockCount=" + endBlockCount;
+    assert totalBlockCount == floorSubBlockCount + nonFloorBlockCount: "floorSubBlockCount=" + floorSubBlockCount + " nonFloorBlockCount=" + nonFloorBlockCount + " totalBlockCount=" + totalBlockCount;
+    assert totalBlockCount == mixedBlockCount + termsOnlyBlockCount + subBlocksOnlyBlockCount: "totalBlockCount=" + totalBlockCount + " mixedBlockCount=" + mixedBlockCount + " subBlocksOnlyBlockCount=" + subBlocksOnlyBlockCount + " termsOnlyBlockCount=" + termsOnlyBlockCount;
+  }
+
+  @Override
+  public String toString() {
+    final ByteArrayOutputStream bos = new ByteArrayOutputStream(1024);
+    PrintStream out;
+    try {
+      out = new PrintStream(bos, false, IOUtils.UTF_8);
+    } catch (UnsupportedEncodingException bogus) {
+      throw new RuntimeException(bogus);
+    }
+      
+    out.println("  index FST:");
+    out.println("    " + indexNumBytes + " bytes");
+    out.println("  terms:");
+    out.println("    " + totalTermCount + " terms");
+    out.println("    " + totalTermBytes + " bytes" + (totalTermCount != 0 ? " (" + String.format(Locale.ROOT, "%.1f", ((double) totalTermBytes)/totalTermCount) + " bytes/term)" : ""));
+    out.println("  blocks:");
+    out.println("    " + totalBlockCount + " blocks");
+    out.println("    " + termsOnlyBlockCount + " terms-only blocks");
+    out.println("    " + subBlocksOnlyBlockCount + " sub-block-only blocks");
+    out.println("    " + mixedBlockCount + " mixed blocks");
+    out.println("    " + floorBlockCount + " floor blocks");
+    out.println("    " + (totalBlockCount-floorSubBlockCount) + " non-floor blocks");
+    out.println("    " + floorSubBlockCount + " floor sub-blocks");
+    out.println("    " + totalBlockSuffixBytes + " term suffix bytes" + (totalBlockCount != 0 ? " (" + String.format(Locale.ROOT, "%.1f", ((double) totalBlockSuffixBytes)/totalBlockCount) + " suffix-bytes/block)" : ""));
+    out.println("    " + totalBlockStatsBytes + " term stats bytes" + (totalBlockCount != 0 ? " (" + String.format(Locale.ROOT, "%.1f", ((double) totalBlockStatsBytes)/totalBlockCount) + " stats-bytes/block)" : ""));
+    out.println("    " + totalBlockOtherBytes + " other bytes" + (totalBlockCount != 0 ? " (" + String.format(Locale.ROOT, "%.1f", ((double) totalBlockOtherBytes)/totalBlockCount) + " other-bytes/block)" : ""));
+    if (totalBlockCount != 0) {
+      out.println("    by prefix length:");
+      int total = 0;
+      for(int prefix=0;prefix<blockCountByPrefixLen.length;prefix++) {
+        final int blockCount = blockCountByPrefixLen[prefix];
+        total += blockCount;
+        if (blockCount != 0) {
+          out.println("      " + String.format(Locale.ROOT, "%2d", prefix) + ": " + blockCount);
+        }
+      }
+      assert totalBlockCount == total;
+    }
+
+    try {
+      return bos.toString(IOUtils.UTF_8);
+    } catch (UnsupportedEncodingException bogus) {
+      throw new RuntimeException(bogus);
+    }
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/CompressingStoredFieldsIndexReader.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/CompressingStoredFieldsIndexReader.java
new file mode 100644
index 0000000..92f2498
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/CompressingStoredFieldsIndexReader.java
@@ -0,0 +1,216 @@
+package org.apache.lucene.codecs.encrypted.compressing;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.compressing.CompressingStoredFieldsIndexWriter;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.SegmentInfo;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.Accountables;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.RamUsageEstimator;
+import org.apache.lucene.util.packed.PackedInts;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import static org.apache.lucene.util.BitUtil.zigZagDecode;
+
+/**
+ * Random-access reader for {@link CompressingStoredFieldsIndexWriter}.
+ * @lucene.internal
+ */
+public final class CompressingStoredFieldsIndexReader implements Cloneable, Accountable {
+
+  private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(CompressingStoredFieldsIndexReader.class);
+
+  final int maxDoc;
+  final int[] docBases;
+  final long[] startPointers;
+  final int[] avgChunkDocs;
+  final long[] avgChunkSizes;
+  final PackedInts.Reader[] docBasesDeltas; // delta from the avg
+  final PackedInts.Reader[] startPointersDeltas; // delta from the avg
+
+  // It is the responsibility of the caller to close fieldsIndexIn after this constructor
+  // has been called
+  CompressingStoredFieldsIndexReader(IndexInput fieldsIndexIn, SegmentInfo si) throws IOException {
+    maxDoc = si.maxDoc();
+    int[] docBases = new int[16];
+    long[] startPointers = new long[16];
+    int[] avgChunkDocs = new int[16];
+    long[] avgChunkSizes = new long[16];
+    PackedInts.Reader[] docBasesDeltas = new PackedInts.Reader[16];
+    PackedInts.Reader[] startPointersDeltas = new PackedInts.Reader[16];
+
+    final int packedIntsVersion = fieldsIndexIn.readVInt();
+
+    int blockCount = 0;
+
+    for (;;) {
+      final int numChunks = fieldsIndexIn.readVInt();
+      if (numChunks == 0) {
+        break;
+      }
+      if (blockCount == docBases.length) {
+        final int newSize = ArrayUtil.oversize(blockCount + 1, 8);
+        docBases = Arrays.copyOf(docBases, newSize);
+        startPointers = Arrays.copyOf(startPointers, newSize);
+        avgChunkDocs = Arrays.copyOf(avgChunkDocs, newSize);
+        avgChunkSizes = Arrays.copyOf(avgChunkSizes, newSize);
+        docBasesDeltas = Arrays.copyOf(docBasesDeltas, newSize);
+        startPointersDeltas = Arrays.copyOf(startPointersDeltas, newSize);
+      }
+
+      // doc bases
+      docBases[blockCount] = fieldsIndexIn.readVInt();
+      avgChunkDocs[blockCount] = fieldsIndexIn.readVInt();
+      final int bitsPerDocBase = fieldsIndexIn.readVInt();
+      if (bitsPerDocBase > 32) {
+        throw new CorruptIndexException("Corrupted bitsPerDocBase: " + bitsPerDocBase, fieldsIndexIn);
+      }
+      docBasesDeltas[blockCount] = PackedInts.getReaderNoHeader(fieldsIndexIn, PackedInts.Format.PACKED, packedIntsVersion, numChunks, bitsPerDocBase);
+
+      // start pointers
+      startPointers[blockCount] = fieldsIndexIn.readVLong();
+      avgChunkSizes[blockCount] = fieldsIndexIn.readVLong();
+      final int bitsPerStartPointer = fieldsIndexIn.readVInt();
+      if (bitsPerStartPointer > 64) {
+        throw new CorruptIndexException("Corrupted bitsPerStartPointer: " + bitsPerStartPointer, fieldsIndexIn);
+      }
+      startPointersDeltas[blockCount] = PackedInts.getReaderNoHeader(fieldsIndexIn, PackedInts.Format.PACKED, packedIntsVersion, numChunks, bitsPerStartPointer);
+
+      ++blockCount;
+    }
+
+    this.docBases = Arrays.copyOf(docBases, blockCount);
+    this.startPointers = Arrays.copyOf(startPointers, blockCount);
+    this.avgChunkDocs = Arrays.copyOf(avgChunkDocs, blockCount);
+    this.avgChunkSizes = Arrays.copyOf(avgChunkSizes, blockCount);
+    this.docBasesDeltas = Arrays.copyOf(docBasesDeltas, blockCount);
+    this.startPointersDeltas = Arrays.copyOf(startPointersDeltas, blockCount);
+  }
+
+  private int block(int docID) {
+    int lo = 0, hi = docBases.length - 1;
+    while (lo <= hi) {
+      final int mid = (lo + hi) >>> 1;
+      final int midValue = docBases[mid];
+      if (midValue == docID) {
+        return mid;
+      } else if (midValue < docID) {
+        lo = mid + 1;
+      } else {
+        hi = mid - 1;
+      }
+    }
+    return hi;
+  }
+
+  private int relativeDocBase(int block, int relativeChunk) {
+    final int expected = avgChunkDocs[block] * relativeChunk;
+    final long delta = zigZagDecode(docBasesDeltas[block].get(relativeChunk));
+    return expected + (int) delta;
+  }
+
+  private long relativeStartPointer(int block, int relativeChunk) {
+    final long expected = avgChunkSizes[block] * relativeChunk;
+    final long delta = zigZagDecode(startPointersDeltas[block].get(relativeChunk));
+    return expected + delta;
+  }
+
+  private int relativeChunk(int block, int relativeDoc) {
+    int lo = 0, hi = docBasesDeltas[block].size() - 1;
+    while (lo <= hi) {
+      final int mid = (lo + hi) >>> 1;
+      final int midValue = relativeDocBase(block, mid);
+      if (midValue == relativeDoc) {
+        return mid;
+      } else if (midValue < relativeDoc) {
+        lo = mid + 1;
+      } else {
+        hi = mid - 1;
+      }
+    }
+    return hi;
+  }
+
+  long getStartPointer(int docID) {
+    if (docID < 0 || docID >= maxDoc) {
+      throw new IllegalArgumentException("docID out of range [0-" + maxDoc + "]: " + docID);
+    }
+    final int block = block(docID);
+    final int relativeChunk = relativeChunk(block, docID - docBases[block]);
+    return startPointers[block] + relativeStartPointer(block, relativeChunk);
+  }
+
+  @Override
+  public CompressingStoredFieldsIndexReader clone() {
+    return this;
+  }
+
+  @Override
+  public long ramBytesUsed() {
+    long res = BASE_RAM_BYTES_USED;
+
+    res += RamUsageEstimator.shallowSizeOf(docBasesDeltas);
+    for (PackedInts.Reader r : docBasesDeltas) {
+      res += r.ramBytesUsed();
+    }
+    res += RamUsageEstimator.shallowSizeOf(startPointersDeltas);
+    for (PackedInts.Reader r : startPointersDeltas) {
+      res += r.ramBytesUsed();
+    }
+
+    res += RamUsageEstimator.sizeOf(docBases);
+    res += RamUsageEstimator.sizeOf(startPointers);
+    res += RamUsageEstimator.sizeOf(avgChunkDocs); 
+    res += RamUsageEstimator.sizeOf(avgChunkSizes);
+
+    return res;
+  }
+
+  @Override
+  public Collection<Accountable> getChildResources() {
+    List<Accountable> resources = new ArrayList<>();
+    
+    long docBaseDeltaBytes = RamUsageEstimator.shallowSizeOf(docBasesDeltas);
+    for (PackedInts.Reader r : docBasesDeltas) {
+      docBaseDeltaBytes += r.ramBytesUsed();
+    }
+    resources.add(Accountables.namedAccountable("doc base deltas", docBaseDeltaBytes));
+    
+    long startPointerDeltaBytes = RamUsageEstimator.shallowSizeOf(startPointersDeltas);
+    for (PackedInts.Reader r : startPointersDeltas) {
+      startPointerDeltaBytes += r.ramBytesUsed();
+    }
+    resources.add(Accountables.namedAccountable("start pointer deltas", startPointerDeltaBytes));
+    
+    return Collections.unmodifiableList(resources);
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + "(blocks=" + docBases.length + ")";
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/CompressingStoredFieldsIndexWriter.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/CompressingStoredFieldsIndexWriter.java
new file mode 100644
index 0000000..ffe189c
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/CompressingStoredFieldsIndexWriter.java
@@ -0,0 +1,213 @@
+package org.apache.lucene.codecs.encrypted.compressing;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.Codec;
+import org.apache.lucene.codecs.CodecUtil;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.packed.PackedInts;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+import static org.apache.lucene.util.BitUtil.zigZagEncode;
+
+/**
+ * Efficient index format for block-based {@link Codec}s.
+ * <p> This writer generates a file which can be loaded into memory using
+ * memory-efficient data structures to quickly locate the block that contains
+ * any document.
+ * <p>In order to have a compact in-memory representation, for every block of
+ * 1024 chunks, this index computes the average number of bytes per
+ * chunk and for every chunk, only stores the difference between<ul>
+ * <li>${chunk number} * ${average length of a chunk}</li>
+ * <li>and the actual start offset of the chunk</li></ul>
+ * <p>Data is written as follows:
+ * <ul>
+ * <li>PackedIntsVersion, &lt;Block&gt;<sup>BlockCount</sup>, BlocksEndMarker</li>
+ * <li>PackedIntsVersion --&gt; {@link PackedInts#VERSION_CURRENT} as a {@link DataOutput#writeVInt VInt}</li>
+ * <li>BlocksEndMarker --&gt; <tt>0</tt> as a {@link DataOutput#writeVInt VInt}, this marks the end of blocks since blocks are not allowed to start with <tt>0</tt></li>
+ * <li>Block --&gt; BlockChunks, &lt;DocBases&gt;, &lt;StartPointers&gt;</li>
+ * <li>BlockChunks --&gt; a {@link DataOutput#writeVInt VInt} which is the number of chunks encoded in the block</li>
+ * <li>DocBases --&gt; DocBase, AvgChunkDocs, BitsPerDocBaseDelta, DocBaseDeltas</li>
+ * <li>DocBase --&gt; first document ID of the block of chunks, as a {@link DataOutput#writeVInt VInt}</li>
+ * <li>AvgChunkDocs --&gt; average number of documents in a single chunk, as a {@link DataOutput#writeVInt VInt}</li>
+ * <li>BitsPerDocBaseDelta --&gt; number of bits required to represent a delta from the average using <a href="https://developers.google.com/protocol-buffers/docs/encoding#types">ZigZag encoding</a></li>
+ * <li>DocBaseDeltas --&gt; {@link PackedInts packed} array of BlockChunks elements of BitsPerDocBaseDelta bits each, representing the deltas from the average doc base using <a href="https://developers.google.com/protocol-buffers/docs/encoding#types">ZigZag encoding</a>.</li>
+ * <li>StartPointers --&gt; StartPointerBase, AvgChunkSize, BitsPerStartPointerDelta, StartPointerDeltas</li>
+ * <li>StartPointerBase --&gt; the first start pointer of the block, as a {@link DataOutput#writeVLong VLong}</li>
+ * <li>AvgChunkSize --&gt; the average size of a chunk of compressed documents, as a {@link DataOutput#writeVLong VLong}</li>
+ * <li>BitsPerStartPointerDelta --&gt; number of bits required to represent a delta from the average using <a href="https://developers.google.com/protocol-buffers/docs/encoding#types">ZigZag encoding</a></li>
+ * <li>StartPointerDeltas --&gt; {@link PackedInts packed} array of BlockChunks elements of BitsPerStartPointerDelta bits each, representing the deltas from the average start pointer using <a href="https://developers.google.com/protocol-buffers/docs/encoding#types">ZigZag encoding</a></li>
+ * <li>Footer --&gt; {@link CodecUtil#writeFooter CodecFooter}</li>
+ * </ul>
+ * <p>Notes
+ * <ul>
+ * <li>For any block, the doc base of the n-th chunk can be restored with
+ * <code>DocBase + AvgChunkDocs * n + DocBaseDeltas[n]</code>.</li>
+ * <li>For any block, the start pointer of the n-th chunk can be restored with
+ * <code>StartPointerBase + AvgChunkSize * n + StartPointerDeltas[n]</code>.</li>
+ * <li>Once data is loaded into memory, you can lookup the start pointer of any
+ * document by performing two binary searches: a first one based on the values
+ * of DocBase in order to find the right block, and then inside the block based
+ * on DocBaseDeltas (by reconstructing the doc bases for every chunk).</li>
+ * </ul>
+ * @lucene.internal
+ */
+public final class CompressingStoredFieldsIndexWriter implements Closeable {
+  
+  final IndexOutput fieldsIndexOut;
+  final int blockSize;
+  int totalDocs;
+  int blockDocs;
+  int blockChunks;
+  long firstStartPointer;
+  long maxStartPointer;
+  final int[] docBaseDeltas;
+  final long[] startPointerDeltas;
+
+  CompressingStoredFieldsIndexWriter(IndexOutput indexOutput, int blockSize) throws IOException {
+    if (blockSize <= 0) {
+      throw new IllegalArgumentException("blockSize must be positive");
+    }
+    this.blockSize = blockSize;
+    this.fieldsIndexOut = indexOutput;
+    reset();
+    totalDocs = 0;
+    docBaseDeltas = new int[blockSize];
+    startPointerDeltas = new long[blockSize];
+    fieldsIndexOut.writeVInt(PackedInts.VERSION_CURRENT);
+  }
+
+  private void reset() {
+    blockChunks = 0;
+    blockDocs = 0;
+    firstStartPointer = -1; // means unset
+  }
+
+  private void writeBlock() throws IOException {
+    assert blockChunks > 0;
+    fieldsIndexOut.writeVInt(blockChunks);
+
+    // The trick here is that we only store the difference from the average start
+    // pointer or doc base, this helps save bits per value.
+    // And in order to prevent a few chunks that would be far from the average to
+    // raise the number of bits per value for all of them, we only encode blocks
+    // of 1024 chunks at once
+    // See LUCENE-4512
+
+    // doc bases
+    final int avgChunkDocs;
+    if (blockChunks == 1) {
+      avgChunkDocs = 0;
+    } else {
+      avgChunkDocs = Math.round((float) (blockDocs - docBaseDeltas[blockChunks - 1]) / (blockChunks - 1));
+    }
+    fieldsIndexOut.writeVInt(totalDocs - blockDocs); // docBase
+    fieldsIndexOut.writeVInt(avgChunkDocs);
+    int docBase = 0;
+    long maxDelta = 0;
+    for (int i = 0; i < blockChunks; ++i) {
+      final int delta = docBase - avgChunkDocs * i;
+      maxDelta |= zigZagEncode(delta);
+      docBase += docBaseDeltas[i];
+    }
+
+    final int bitsPerDocBase = PackedInts.bitsRequired(maxDelta);
+    fieldsIndexOut.writeVInt(bitsPerDocBase);
+    PackedInts.Writer writer = PackedInts.getWriterNoHeader(fieldsIndexOut,
+        PackedInts.Format.PACKED, blockChunks, bitsPerDocBase, 1);
+    docBase = 0;
+    for (int i = 0; i < blockChunks; ++i) {
+      final long delta = docBase - avgChunkDocs * i;
+      assert PackedInts.bitsRequired(zigZagEncode(delta)) <= writer.bitsPerValue();
+      writer.add(zigZagEncode(delta));
+      docBase += docBaseDeltas[i];
+    }
+    writer.finish();
+
+    // start pointers
+    fieldsIndexOut.writeVLong(firstStartPointer);
+    final long avgChunkSize;
+    if (blockChunks == 1) {
+      avgChunkSize = 0;
+    } else {
+      avgChunkSize = (maxStartPointer - firstStartPointer) / (blockChunks - 1);
+    }
+    fieldsIndexOut.writeVLong(avgChunkSize);
+    long startPointer = 0;
+    maxDelta = 0;
+    for (int i = 0; i < blockChunks; ++i) {
+      startPointer += startPointerDeltas[i];
+      final long delta = startPointer - avgChunkSize * i;
+      maxDelta |= zigZagEncode(delta);
+    }
+
+    final int bitsPerStartPointer = PackedInts.bitsRequired(maxDelta);
+    fieldsIndexOut.writeVInt(bitsPerStartPointer);
+    writer = PackedInts.getWriterNoHeader(fieldsIndexOut, PackedInts.Format.PACKED,
+        blockChunks, bitsPerStartPointer, 1);
+    startPointer = 0;
+    for (int i = 0; i < blockChunks; ++i) {
+      startPointer += startPointerDeltas[i];
+      final long delta = startPointer - avgChunkSize * i;
+      assert PackedInts.bitsRequired(zigZagEncode(delta)) <= writer.bitsPerValue();
+      writer.add(zigZagEncode(delta));
+    }
+    writer.finish();
+  }
+
+  void writeIndex(int numDocs, long startPointer) throws IOException {
+    if (blockChunks == blockSize) {
+      writeBlock();
+      reset();
+    }
+
+    if (firstStartPointer == -1) {
+      firstStartPointer = maxStartPointer = startPointer;
+    }
+    assert firstStartPointer > 0 && startPointer >= firstStartPointer;
+
+    docBaseDeltas[blockChunks] = numDocs;
+    startPointerDeltas[blockChunks] = startPointer - maxStartPointer;
+
+    ++blockChunks;
+    blockDocs += numDocs;
+    totalDocs += numDocs;
+    maxStartPointer = startPointer;
+  }
+
+  void finish(int numDocs, long maxPointer) throws IOException {
+    if (numDocs != totalDocs) {
+      throw new IllegalStateException("Expected " + numDocs + " docs, but got " + totalDocs);
+    }
+    if (blockChunks > 0) {
+      writeBlock();
+    }
+    fieldsIndexOut.writeVInt(0); // end marker
+    fieldsIndexOut.writeVLong(maxPointer);
+    CodecUtil.writeFooter(fieldsIndexOut);
+  }
+
+  @Override
+  public void close() throws IOException {
+    fieldsIndexOut.close();
+  }
+
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/DecipherDecompressor.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/DecipherDecompressor.java
new file mode 100644
index 0000000..172f6c4
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/DecipherDecompressor.java
@@ -0,0 +1,91 @@
+package org.apache.lucene.codecs.encrypted.compressing;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.compressing.Decompressor;
+import org.apache.lucene.codecs.encrypted.CipherFactory;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.store.DataInput;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+
+import javax.crypto.Cipher;
+import java.io.IOException;
+
+/**
+ * A decompressor that will first decipher the compressed data block before decompressing.
+ */
+class DecipherDecompressor extends Decompressor {
+
+  private final CipherFactory cipherFactory;
+  private final long keyVersion;
+
+  private final Decompressor decompressor;
+  private Cipher decipher;
+
+  /**
+   * Buffer for deciphering data block
+   */
+  private BytesRef encryptedBytesBuffer = new BytesRef(512);
+
+  /**
+   * The file pointer of the last deciphered data block
+   */
+  private long lastStartPointer = -1;
+
+  DecipherDecompressor(CipherFactory cipherFactory, long keyVersion, Decompressor decompressor) {
+    this.decompressor = decompressor;
+    this.cipherFactory = cipherFactory;
+    this.keyVersion = keyVersion;
+    this.decipher = cipherFactory.newDecipherInstance(keyVersion);
+  }
+
+  @Override
+  public void decompress(DataInput in, int originalLength, int offset, int length, BytesRef bytes) throws IOException {
+    // todo: instead of deciphering everything, we could use the block size to decipher only what we need
+    long fp = ((IndexInput) in).getFilePointer();
+    if (lastStartPointer != fp) { // we have not yet deciphered this new data block
+      lastStartPointer = fp;
+      int encryptLength = in.readVInt();
+      if (encryptLength > encryptedBytesBuffer.bytes.length) {
+        encryptedBytesBuffer.bytes = ArrayUtil.grow(encryptedBytesBuffer.bytes, encryptLength);
+      }
+      in.readBytes(encryptedBytesBuffer.bytes, 0, encryptLength);
+      try {
+        // we can reuse encryptedBytesBuffer as output since the decrypt method is copy-safe
+        encryptedBytesBuffer.length = decipher.doFinal(encryptedBytesBuffer.bytes, 0, encryptLength, encryptedBytesBuffer.bytes, 0);
+      } catch (Exception e) {
+        throw new IOException("Deciphering of compressed stored fields block failed", e);
+      }
+    }
+    else { // if we have already deciphered the data block, just skip it
+      IndexInput indexInput = (IndexInput) in;
+      int encryptLength = indexInput.readVInt();
+      indexInput.seek(indexInput.getFilePointer() + encryptLength);
+    }
+    ByteArrayDataInput input = new ByteArrayDataInput(encryptedBytesBuffer.bytes, 0, encryptedBytesBuffer.length);
+    decompressor.decompress(input, originalLength, offset, length, bytes);
+  }
+
+  @Override
+  public Decompressor clone() {
+    return new DecipherDecompressor(cipherFactory, keyVersion, decompressor);
+  }
+
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncipherCompressor.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncipherCompressor.java
new file mode 100644
index 0000000..48e8349
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncipherCompressor.java
@@ -0,0 +1,67 @@
+package org.apache.lucene.codecs.encrypted.compressing;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.compressing.Compressor;
+import org.apache.lucene.codecs.compressing.GrowableByteArrayDataOutput;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.util.ArrayUtil;
+
+import javax.crypto.Cipher;
+import java.io.IOException;
+
+/**
+ * A compressor that will compress the data block then encipher it.
+ */
+public class EncipherCompressor extends Compressor {
+
+  private final Compressor compressor;
+
+  private final Cipher encipher;
+
+  /**
+   * Reusable byte buffer for compressing and encrypting
+   */
+  private GrowableByteArrayDataOutput compressedBytesBuffer = new GrowableByteArrayDataOutput(512);
+
+  EncipherCompressor(Cipher encipher, Compressor compressor) {
+    this.encipher = encipher;
+    this.compressor = compressor;
+  }
+
+  @Override
+  public void compress(byte[] bytes, int off, int len, DataOutput out) throws IOException {
+    compressedBytesBuffer.length = 0; // reset buffer
+    compressor.compress(bytes, off, len, compressedBytesBuffer);
+    int size = encipher.getOutputSize(compressedBytesBuffer.length);
+    // ensure that the compressedBytesBuffer is large enough to accept encrypted bytes
+    if (size > compressedBytesBuffer.bytes.length) {
+      compressedBytesBuffer.bytes = ArrayUtil.grow(compressedBytesBuffer.bytes, size);
+    }
+    int written = 0;
+    try {
+      // we can reuse the same buffer for input and output since encrypt is copy safe
+      written = encipher.doFinal(compressedBytesBuffer.bytes, 0, compressedBytesBuffer.length, compressedBytesBuffer.bytes, 0);
+    } catch (Exception e) {
+      throw new IOException("Enciphering of compressed stored fields block failed", e);
+    }
+
+    out.writeVInt(written);
+    out.writeBytes(compressedBytesBuffer.bytes, 0, written);
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingStoredFieldsFormat.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingStoredFieldsFormat.java
new file mode 100644
index 0000000..1a3d6d3
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingStoredFieldsFormat.java
@@ -0,0 +1,165 @@
+package org.apache.lucene.codecs.encrypted.compressing;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.CodecUtil;
+import org.apache.lucene.codecs.StoredFieldsFormat;
+import org.apache.lucene.codecs.StoredFieldsReader;
+import org.apache.lucene.codecs.StoredFieldsWriter;
+import org.apache.lucene.codecs.compressing.CompressionMode;
+import org.apache.lucene.codecs.encrypted.CipherFactory;
+import org.apache.lucene.index.FieldInfos;
+import org.apache.lucene.index.MergePolicy;
+import org.apache.lucene.index.SegmentInfo;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.IOContext;
+
+import java.io.IOException;
+
+/**
+ * A {@link StoredFieldsFormat} that compresses documents in chunks in
+ * order to improve the compression ratio and encrypts data on a field-level.
+ * <p>
+ * For a chunk size of <tt>chunkSize</tt> bytes, this {@link StoredFieldsFormat}
+ * does not support documents larger than (<tt>2<sup>31</sup> - chunkSize</tt>)
+ * bytes.
+ * <p>
+ * For optimal performance, you should use a {@link MergePolicy} that returns
+ * segments that have the biggest byte size first.
+ * @lucene.experimental
+ */
+public abstract class EncryptedCompressingStoredFieldsFormat extends StoredFieldsFormat {
+
+  private final String formatName;
+  private final String segmentSuffix;
+  private final CompressionMode compressionMode;
+  private final int chunkSize;
+  private final int maxDocsPerChunk;
+  private final int blockSize;
+  private final CipherFactory cipherFactory;
+
+  /**
+   * Create a new {@link EncryptedCompressingStoredFieldsFormat} with an empty segment
+   * suffix.
+   *
+   * @see EncryptedCompressingStoredFieldsFormat#EncryptedCompressingStoredFieldsFormat(String, CipherFactory, CompressionMode, int, int, int)
+   */
+  public EncryptedCompressingStoredFieldsFormat(String formatName, CipherFactory cipherFactory,
+                                                CompressionMode compressionMode, int chunkSize, int maxDocsPerChunk,
+                                                int blockSize) {
+    this(formatName, cipherFactory, "", compressionMode, chunkSize, maxDocsPerChunk, blockSize);
+  }
+
+  /**
+   * Create a new {@link EncryptedCompressingStoredFieldsFormat}.
+   * <p>
+   * <code>formatName</code> is the name of the format. This name will be used
+   * in the file formats to perform
+   * {@link CodecUtil#checkIndexHeader codec header checks}.
+   * <p>
+   * <code>segmentSuffix</code> is the segment suffix. This suffix is added to
+   * the result file name only if it's not the empty string.
+   * <p>
+   * The <code>compressionMode</code> parameter allows you to choose between
+   * compression algorithms that have various compression and decompression
+   * speeds so that you can pick the one that best fits your indexing and
+   * searching throughput. You should never instantiate two
+   * {@link EncryptedCompressingStoredFieldsFormat}s that have the same name but
+   * different {@link CompressionMode}s.
+   * <p>
+   * <code>chunkSize</code> is the minimum byte size of a chunk of documents.
+   * A value of <code>1</code> can make sense if there is redundancy across
+   * fields.
+   * <code>maxDocsPerChunk</code> is an upperbound on how many docs may be stored
+   * in a single chunk. This is to bound the cpu costs for highly compressible data.
+   * <p>
+   * Higher values of <code>chunkSize</code> should improve the compression
+   * ratio but will require more memory at indexing time and might make document
+   * loading a little slower (depending on the size of your OS cache compared
+   * to the size of your index).
+   *
+   * @param formatName the name of the {@link StoredFieldsFormat}
+   * @param compressionMode the {@link CompressionMode} to use
+   * @param chunkSize the minimum number of bytes of a single chunk of stored documents
+   * @param maxDocsPerChunk the maximum number of documents in a single chunk
+   * @param blockSize the number of chunks to store in an index block
+   * @see CompressionMode
+   */
+  public EncryptedCompressingStoredFieldsFormat(String formatName, CipherFactory cipherFactory, String segmentSuffix,
+                                                CompressionMode compressionMode, int chunkSize, int maxDocsPerChunk,
+                                                int blockSize) {
+    this.formatName = formatName;
+    this.cipherFactory = cipherFactory;
+    this.segmentSuffix = segmentSuffix;
+    this.compressionMode = compressionMode;
+    if (chunkSize < 1) {
+      throw new IllegalArgumentException("chunkSize must be >= 1");
+    }
+    this.chunkSize = chunkSize;
+    if (maxDocsPerChunk < 1) {
+      throw new IllegalArgumentException("maxDocsPerChunk must be >= 1");
+    }
+    this.maxDocsPerChunk = maxDocsPerChunk;
+    if (blockSize < 1) {
+      throw new IllegalArgumentException("blockSize must be >= 1");
+    }
+    this.blockSize = blockSize;
+  }
+
+  @Override
+  public StoredFieldsReader fieldsReader(Directory directory, SegmentInfo si, FieldInfos fn, IOContext context)
+  throws IOException {
+    String value = si.getAttribute(CipherFactory.CIPHER_VERSION_KEY);
+    if (value == null) {
+      throw new IllegalStateException("missing value for " + CipherFactory.CIPHER_VERSION_KEY +
+          " for segment: " + si.name);
+    }
+
+    return new EncryptedCompressingStoredFieldsReader(cipherFactory, directory, si, segmentSuffix, fn,
+        context, formatName, compressionMode);
+  }
+
+  @Override
+  public StoredFieldsWriter fieldsWriter(Directory directory, SegmentInfo si, IOContext context) throws IOException {
+    // Store cipher version in segment info
+    long cipherVersion = this.getCipherFactory().getEncipherVersion();
+    // It might overwrite the value put by another format, e.g., EncryptedLucene50PostingsFormat
+    // We just check that it is the same version.
+    String previous = si.putAttribute(CipherFactory.CIPHER_VERSION_KEY, Long.toString(cipherVersion));
+    if (previous != null && Long.parseLong(previous) != cipherVersion) {
+      throw new IllegalStateException("found existing value for " + CipherFactory.CIPHER_VERSION_KEY +
+          " for segment: " + si.name + "old=" + previous + ", new=" + cipherVersion);
+    }
+
+    return new EncryptedCompressingStoredFieldsWriter(this, directory, si, segmentSuffix, context,
+          formatName, compressionMode, chunkSize, maxDocsPerChunk, blockSize);
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + "(compressionMode=" + compressionMode
+        + ", chunkSize=" + chunkSize + ", maxDocsPerChunk=" + maxDocsPerChunk + ", blockSize=" + blockSize + ")";
+  }
+
+  public CipherFactory getCipherFactory() {
+    return cipherFactory;
+  }
+
+  public abstract boolean isFieldEncrypted(String field);
+
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingStoredFieldsReader.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingStoredFieldsReader.java
new file mode 100644
index 0000000..186c468
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingStoredFieldsReader.java
@@ -0,0 +1,758 @@
+package org.apache.lucene.codecs.encrypted.compressing;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.CodecUtil;
+import org.apache.lucene.codecs.StoredFieldsReader;
+import org.apache.lucene.codecs.compressing.CompressionMode;
+import org.apache.lucene.codecs.compressing.Decompressor;
+import org.apache.lucene.codecs.encrypted.CipherFactory;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.FieldInfo;
+import org.apache.lucene.index.FieldInfos;
+import org.apache.lucene.index.IndexFileNames;
+import org.apache.lucene.index.SegmentInfo;
+import org.apache.lucene.index.StoredFieldVisitor;
+import org.apache.lucene.store.AlreadyClosedException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.store.ChecksumIndexInput;
+import org.apache.lucene.store.DataInput;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.IOContext;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.Accountables;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BitUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.IOUtils;
+import org.apache.lucene.util.IntsRef;
+import org.apache.lucene.util.packed.PackedInts;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * {@link StoredFieldsReader} impl for {@link EncryptedCompressingStoredFieldsFormat}.
+ * @lucene.experimental
+ */
+public final class EncryptedCompressingStoredFieldsReader extends StoredFieldsReader {
+
+  private final int version;
+  private final long cipherVersion;
+  private final FieldInfos fieldInfos;
+  private final CompressingStoredFieldsIndexReader indexReader;
+  private final long maxPointer;
+  private final IndexInput fieldsStream;
+  private final int chunkSize;
+  private final int packedIntsVersion;
+  private final CompressionMode compressionMode;
+  private final Decompressor decompressor;
+  private final Decompressor decipherDecompressor;
+  private final int numDocs;
+  private final boolean merging;
+  private final BlockState state;
+  private final long numChunks; // number of compressed blocks written
+  private final long numDirtyChunks; // number of incomplete compressed blocks written
+  private boolean closed;
+
+  // used by clone
+  private EncryptedCompressingStoredFieldsReader(EncryptedCompressingStoredFieldsReader reader, boolean merging) {
+    this.version = reader.version;
+    this.cipherVersion = reader.cipherVersion;
+    this.fieldInfos = reader.fieldInfos;
+    this.fieldsStream = reader.fieldsStream.clone();
+    this.indexReader = reader.indexReader.clone();
+    this.maxPointer = reader.maxPointer;
+    this.chunkSize = reader.chunkSize;
+    this.packedIntsVersion = reader.packedIntsVersion;
+    this.compressionMode = reader.compressionMode;
+    this.decompressor = reader.decompressor.clone();
+    this.decipherDecompressor = reader.decipherDecompressor.clone();
+    this.numDocs = reader.numDocs;
+    this.numChunks = reader.numChunks;
+    this.numDirtyChunks = reader.numDirtyChunks;
+    this.merging = merging;
+    this.state = new BlockState();
+    this.closed = false;
+  }
+
+  /** Sole constructor. */
+  public EncryptedCompressingStoredFieldsReader(CipherFactory cipherFactory, Directory d, SegmentInfo si, String segmentSuffix, FieldInfos fn,
+                                                IOContext context, String formatName, CompressionMode compressionMode) throws IOException {
+    this.compressionMode = compressionMode;
+    final String segment = si.name;
+    boolean success = false;
+    fieldInfos = fn;
+    numDocs = si.maxDoc();
+    
+    int version = -1;
+    long maxPointer = -1;
+    CompressingStoredFieldsIndexReader indexReader = null;
+    
+    // Load the index into memory
+    final String indexName = IndexFileNames.segmentFileName(segment, segmentSuffix, EncryptedCompressingStoredFieldsWriter.FIELDS_INDEX_EXTENSION);
+    try (ChecksumIndexInput indexStream = d.openChecksumInput(indexName, context)) {
+      Throwable priorE = null;
+      try {
+        final String codecNameIdx = formatName + EncryptedCompressingStoredFieldsWriter.CODEC_SFX_IDX;
+        version = CodecUtil.checkIndexHeader(indexStream, codecNameIdx, EncryptedCompressingStoredFieldsWriter.VERSION_START, EncryptedCompressingStoredFieldsWriter.VERSION_CURRENT, si.getId(), segmentSuffix);
+        assert CodecUtil.indexHeaderLength(codecNameIdx, segmentSuffix) == indexStream.getFilePointer();
+        indexReader = new CompressingStoredFieldsIndexReader(indexStream, si);
+        maxPointer = indexStream.readVLong();
+      } catch (Throwable exception) {
+        priorE = exception;
+      } finally {
+        CodecUtil.checkFooter(indexStream, priorE);
+      }
+    }
+    
+    this.version = version;
+    this.maxPointer = maxPointer;
+    this.indexReader = indexReader;
+
+    final String fieldsStreamFN = IndexFileNames.segmentFileName(segment, segmentSuffix, EncryptedCompressingStoredFieldsWriter.FIELDS_EXTENSION);
+    try {
+      // Open the data file and read metadata
+      fieldsStream = d.openInput(fieldsStreamFN, context);
+      final String codecNameDat = formatName + EncryptedCompressingStoredFieldsWriter.CODEC_SFX_DAT;
+      final int fieldsVersion = CodecUtil.checkIndexHeader(fieldsStream, codecNameDat, EncryptedCompressingStoredFieldsWriter.VERSION_START, EncryptedCompressingStoredFieldsWriter.VERSION_CURRENT, si.getId(), segmentSuffix);
+      if (version != fieldsVersion) {
+        throw new CorruptIndexException("Version mismatch between stored fields index and data: " + version + " != " + fieldsVersion, fieldsStream);
+      }
+      assert CodecUtil.indexHeaderLength(codecNameDat, segmentSuffix) == fieldsStream.getFilePointer();
+
+      chunkSize = fieldsStream.readVInt();
+      packedIntsVersion = fieldsStream.readVInt();
+
+      this.merging = false;
+      this.state = new BlockState();
+      
+      if (version >= EncryptedCompressingStoredFieldsWriter.VERSION_CHUNK_STATS) {
+        fieldsStream.seek(maxPointer);
+        numChunks = fieldsStream.readVLong();
+        numDirtyChunks = fieldsStream.readVLong();
+        if (numDirtyChunks > numChunks) {
+          throw new CorruptIndexException("invalid chunk counts: dirty=" + numDirtyChunks + ", total=" + numChunks, fieldsStream);
+        }
+      } else {
+        numChunks = numDirtyChunks = -1;
+      }
+      
+      // NOTE: data file is too costly to verify checksum against all the bytes on open,
+      // but for now we at least verify proper structure of the checksum footer: which looks
+      // for FOOTER_MAGIC + algorithmID. This is cheap and can detect some forms of corruption
+      // such as file truncation.
+      CodecUtil.retrieveChecksum(fieldsStream);
+
+      // Read the cipher version from the segment info and instantiate the thread local variable for the decipher
+      cipherVersion = Long.parseLong(si.getAttribute(CipherFactory.CIPHER_VERSION_KEY));
+      decompressor = compressionMode.newDecompressor();
+      decipherDecompressor = new DecipherDecompressor(cipherFactory, cipherVersion, decompressor);
+
+      success = true;
+    } finally {
+      if (!success) {
+        IOUtils.closeWhileHandlingException(this);
+      }
+    }
+  }
+
+  /**
+   * @throws AlreadyClosedException if this FieldsReader is closed
+   */
+  private void ensureOpen() throws AlreadyClosedException {
+    if (closed) {
+      throw new AlreadyClosedException("this FieldsReader is closed");
+    }
+  }
+
+  /** 
+   * Close the underlying {@link IndexInput}s.
+   */
+  @Override
+  public void close() throws IOException {
+    if (!closed) {
+      IOUtils.close(fieldsStream);
+      closed = true;
+    }
+  }
+
+  private static void readField(DataInput in, StoredFieldVisitor visitor, FieldInfo info, int bits) throws IOException {
+    switch (bits & EncryptedCompressingStoredFieldsWriter.TYPE_MASK) {
+      case EncryptedCompressingStoredFieldsWriter.BYTE_ARR:
+        int length = in.readVInt();
+        byte[] data = new byte[length];
+        in.readBytes(data, 0, length);
+        visitor.binaryField(info, data);
+        break;
+      case EncryptedCompressingStoredFieldsWriter.STRING:
+        length = in.readVInt();
+        data = new byte[length];
+        in.readBytes(data, 0, length);
+        visitor.stringField(info, data);
+        break;
+      case EncryptedCompressingStoredFieldsWriter.NUMERIC_INT:
+        visitor.intField(info, in.readZInt());
+        break;
+      case EncryptedCompressingStoredFieldsWriter.NUMERIC_FLOAT:
+        visitor.floatField(info, readZFloat(in));
+        break;
+      case EncryptedCompressingStoredFieldsWriter.NUMERIC_LONG:
+        visitor.longField(info, readTLong(in));
+        break;
+      case EncryptedCompressingStoredFieldsWriter.NUMERIC_DOUBLE:
+        visitor.doubleField(info, readZDouble(in));
+        break;
+      default:
+        throw new AssertionError("Unknown type flag: " + Integer.toHexString(bits));
+    }
+  }
+
+  private static void skipField(DataInput in, int bits) throws IOException {
+    switch (bits & EncryptedCompressingStoredFieldsWriter.TYPE_MASK) {
+      case EncryptedCompressingStoredFieldsWriter.BYTE_ARR:
+      case EncryptedCompressingStoredFieldsWriter.STRING:
+        final int length = in.readVInt();
+        in.skipBytes(length);
+        break;
+      case EncryptedCompressingStoredFieldsWriter.NUMERIC_INT:
+        in.readZInt();
+        break;
+      case EncryptedCompressingStoredFieldsWriter.NUMERIC_FLOAT:
+        readZFloat(in);
+        break;
+      case EncryptedCompressingStoredFieldsWriter.NUMERIC_LONG:
+        readTLong(in);
+        break;
+      case EncryptedCompressingStoredFieldsWriter.NUMERIC_DOUBLE:
+        readZDouble(in);
+        break;
+      default:
+        throw new AssertionError("Unknown type flag: " + Integer.toHexString(bits));
+    }
+  }
+
+  /**
+   * Reads a float in a variable-length format.  Reads between one and
+   * five bytes. Small integral values typically take fewer bytes.
+   */
+  static float readZFloat(DataInput in) throws IOException {
+    int b = in.readByte() & 0xFF;
+    if (b == 0xFF) {
+      // negative value
+      return Float.intBitsToFloat(in.readInt());
+    } else if ((b & 0x80) != 0) {
+      // small integer [-1..125]
+      return (b & 0x7f) - 1;
+    } else {
+      // positive float
+      int bits = b << 24 | ((in.readShort() & 0xFFFF) << 8) | (in.readByte() & 0xFF);
+      return Float.intBitsToFloat(bits);
+    }
+  }
+
+  /**
+   * Reads a double in a variable-length format.  Reads between one and
+   * nine bytes. Small integral values typically take fewer bytes.
+   */
+  static double readZDouble(DataInput in) throws IOException {
+    int b = in.readByte() & 0xFF;
+    if (b == 0xFF) {
+      // negative value
+      return Double.longBitsToDouble(in.readLong());
+    } else if (b == 0xFE) {
+      // float
+      return Float.intBitsToFloat(in.readInt());
+    } else if ((b & 0x80) != 0) {
+      // small integer [-1..124]
+      return (b & 0x7f) - 1;
+    } else {
+      // positive double
+      long bits = ((long) b) << 56 | ((in.readInt() & 0xFFFFFFFFL) << 24) | ((in.readShort() & 0xFFFFL) << 8) | (in.readByte() & 0xFFL);
+      return Double.longBitsToDouble(bits);
+    }
+  }
+
+  /**
+   * Reads a long in a variable-length format.  Reads between one and
+   * nine bytes. Small values typically take fewer bytes.
+   */
+  static long readTLong(DataInput in) throws IOException {
+    int header = in.readByte() & 0xFF;
+
+    long bits = header & 0x1F;
+    if ((header & 0x20) != 0) {
+      // continuation bit
+      bits |= in.readVLong() << 5;
+    }
+
+    long l = BitUtil.zigZagDecode(bits);
+
+    switch (header & EncryptedCompressingStoredFieldsWriter.DAY_ENCODING) {
+      case EncryptedCompressingStoredFieldsWriter.SECOND_ENCODING:
+        l *= EncryptedCompressingStoredFieldsWriter.SECOND;
+        break;
+      case EncryptedCompressingStoredFieldsWriter.HOUR_ENCODING:
+        l *= EncryptedCompressingStoredFieldsWriter.HOUR;
+        break;
+      case EncryptedCompressingStoredFieldsWriter.DAY_ENCODING:
+        l *= EncryptedCompressingStoredFieldsWriter.DAY;
+        break;
+      case 0:
+        // uncompressed
+        break;
+      default:
+        throw new AssertionError();
+    }
+
+    return l;
+  }
+
+  /**
+   * A serialized document, you need to decode its input in order to get an actual
+   * {@link Document}.
+   */
+  static class SerializedDocument {
+
+    // the serialized data
+    final DataInput in;
+    final DataInput encryptedIn;
+
+    // the number of bytes on which the document is encoded
+    final int length;
+    final int encryptedLength;
+
+    // the number of stored fields
+    final int numStoredFields;
+    final int numEncryptedStoredFields;
+
+    private SerializedDocument(DataInput in, int length, int numStoredFields, DataInput encryptedIn, int encryptedLength, int numEncryptedStoredFields) {
+      this.in = in;
+      this.length = length;
+      this.numStoredFields = numStoredFields;
+
+      this.encryptedIn = encryptedIn;
+      this.encryptedLength = encryptedLength;
+      this.numEncryptedStoredFields = numEncryptedStoredFields;
+    }
+
+  }
+
+  /**
+   * Keeps state about the current block of documents.
+   */
+  private class BlockState {
+
+    private int docBase, chunkDocs;
+
+    // whether the block has been sliced, this happens for large documents
+    private boolean sliced;
+    private boolean slicedEncrypted;
+    private boolean hasUnencrypted;
+    private boolean hasEncrypted;
+
+    private int[] offsets = IntsRef.EMPTY_INTS;
+    private int[] numStoredFields = IntsRef.EMPTY_INTS;
+
+    private int[] offsetsEncrypted = IntsRef.EMPTY_INTS;
+    private int[] numEncryptedStoredFields = IntsRef.EMPTY_INTS;
+
+    // the start pointer at which you can read the non-encrypted compressed documents
+    private long startPointer;
+    // the start pointer at which you can read the encrypted compressed documents
+    private long startPointerEncrypted;
+
+    private final BytesRef spare = new BytesRef();
+    private final BytesRef bytes = new BytesRef();
+    private final BytesRef bytesEncrypted = new BytesRef();
+
+    boolean contains(int docID) {
+      return docID >= docBase && docID < docBase + chunkDocs;
+    }
+
+    /**
+     * Reset this block so that it stores state for the block
+     * that contains the given doc id.
+     */
+    void reset(int docID) throws IOException {
+      boolean success = false;
+      try {
+        doReset(docID);
+        success = true;
+      } finally {
+        if (success == false) {
+          // if the read failed, set chunkDocs to 0 so that it does not
+          // contain any docs anymore and is not reused. This should help
+          // get consistent exceptions when trying to get several
+          // documents which are in the same corrupted block since it will
+          // force the header to be decoded again
+          chunkDocs = 0;
+        }
+      }
+    }
+
+    private void doReset(int docID) throws IOException {
+      docBase = fieldsStream.readVInt();
+      final long token = fieldsStream.readVLong();
+      chunkDocs = (int) token >>> 4;
+      if (contains(docID) == false
+          || docBase + chunkDocs > numDocs) {
+        throw new CorruptIndexException("Corrupted: docID=" + docID
+            + ", docBase=" + docBase + ", chunkDocs=" + chunkDocs
+            + ", numDocs=" + numDocs, fieldsStream);
+      }
+
+      sliced = (token & 8) != 0;
+      slicedEncrypted = (token & 4) != 0;
+      hasUnencrypted = (token & 2) != 0;
+      hasEncrypted = (token & 1) != 0;
+
+      if (hasUnencrypted) {
+        offsets = ArrayUtil.grow(offsets, chunkDocs + 1);
+        numStoredFields = ArrayUtil.grow(numStoredFields, chunkDocs);
+      }
+      if (hasEncrypted) {
+        offsetsEncrypted = ArrayUtil.grow(offsetsEncrypted, chunkDocs + 1);
+        numEncryptedStoredFields = ArrayUtil.grow(numEncryptedStoredFields, chunkDocs);
+      }
+
+      // read numStoredFields and offsets for docs only if it exists
+      if (hasUnencrypted) {
+        this.readDocHeaders(numStoredFields, offsets);
+      }
+
+      // read numStoredFields and offsets for encrypted docs only if it exists
+      if (hasEncrypted) {
+        this.readDocHeaders(numEncryptedStoredFields, offsetsEncrypted);
+      }
+
+      int unencryptedLength = fieldsStream.readVInt();
+
+      startPointer = fieldsStream.getFilePointer();
+      startPointerEncrypted = startPointer + unencryptedLength;
+
+      // if we are merging, read and decompress eagerly all the docs
+      if (merging) {
+        // Read docs only if it exists
+        if (hasUnencrypted) {
+          int totalLength = offsets[chunkDocs];
+          this.readDocs(totalLength, sliced, bytes, decompressor);
+        }
+
+        // Read encrypted docs only if it exists
+        if (hasEncrypted) {
+          int totalLength = offsetsEncrypted[chunkDocs];
+          this.readDocs(totalLength, slicedEncrypted, bytesEncrypted, decipherDecompressor);
+        }
+      }
+    }
+
+    /**
+     * Read and decompress eagerly all the docs
+     */
+    private void readDocs(int totalLength, boolean sliced, BytesRef dst, Decompressor decompressor) throws IOException {
+      if (sliced) {
+        dst.offset = dst.length = 0;
+        for (int decompressed = 0; decompressed < totalLength; ) {
+          final int toDecompress = Math.min(totalLength - decompressed, chunkSize);
+          decompressor.decompress(fieldsStream, toDecompress, 0, toDecompress, spare);
+          dst.bytes = ArrayUtil.grow(dst.bytes, dst.length + spare.length);
+          System.arraycopy(spare.bytes, spare.offset, dst.bytes, dst.length, spare.length);
+          dst.length += spare.length;
+          decompressed += toDecompress;
+        }
+      } else {
+        decompressor.decompress(fieldsStream, totalLength, 0, totalLength, dst);
+      }
+      if (dst.length != totalLength) {
+        throw new CorruptIndexException("Corrupted: expected chunk size = " + totalLength + ", got " + dst.length, fieldsStream);
+      }
+    }
+
+    private void readDocHeaders(int[] numStoredFields, int[] offsets) throws IOException {
+      if (chunkDocs == 1) {
+        numStoredFields[0] = fieldsStream.readVInt();
+        offsets[1] = fieldsStream.readVInt();
+      } else {
+        // Number of stored fields per document
+        final int bitsPerStoredFields = fieldsStream.readVInt();
+        if (bitsPerStoredFields == 0) {
+          Arrays.fill(numStoredFields, 0, chunkDocs, fieldsStream.readVInt());
+        } else if (bitsPerStoredFields > 31) {
+          throw new CorruptIndexException("bitsPerStoredFields=" + bitsPerStoredFields, fieldsStream);
+        } else {
+          final PackedInts.ReaderIterator it = PackedInts.getReaderIteratorNoHeader(fieldsStream, PackedInts.Format.PACKED, packedIntsVersion, chunkDocs, bitsPerStoredFields, 1);
+          for (int i = 0; i < chunkDocs; ++i) {
+            numStoredFields[i] = (int) it.next();
+          }
+        }
+
+        // The stream encodes the length of each document and we decode
+        // it into a list of monotonically increasing offsets
+        final int bitsPerLength = fieldsStream.readVInt();
+        if (bitsPerLength == 0) {
+          final int length = fieldsStream.readVInt();
+          for (int i = 0; i < chunkDocs; ++i) {
+            offsets[1 + i] = (1 + i) * length;
+          }
+        } else if (bitsPerStoredFields > 31) {
+          throw new CorruptIndexException("bitsPerLength=" + bitsPerLength, fieldsStream);
+        } else {
+          final PackedInts.ReaderIterator it = PackedInts.getReaderIteratorNoHeader(fieldsStream, PackedInts.Format.PACKED, packedIntsVersion, chunkDocs, bitsPerLength, 1);
+          for (int i = 0; i < chunkDocs; ++i) {
+            offsets[i + 1] = (int) it.next();
+          }
+          for (int i = 0; i < chunkDocs; ++i) {
+            offsets[i + 1] += offsets[i];
+          }
+        }
+
+        // Additional validation: only the empty document has a serialized length of 0
+        for (int i = 0; i < chunkDocs; ++i) {
+          final int len = offsets[i + 1] - offsets[i];
+          final int storedFields = numStoredFields[i];
+          if ((len == 0) != (storedFields == 0)) {
+            throw new CorruptIndexException("length=" + len + ", numStoredFields=" + storedFields, fieldsStream);
+          }
+        }
+
+      }
+    }
+
+    /**
+     * Get the serialized representation of the given docID. This docID has
+     * to be contained in the current block.
+     */
+    SerializedDocument document(int docID) throws IOException {
+      if (contains(docID) == false) {
+        throw new IllegalArgumentException();
+      }
+
+      int index = docID - docBase;
+      int length = 0, numStoredFields = 0;
+      int lengthEncrypted = 0, numEncryptedStoredFields = 0;
+      DataInput documentInput, documentEncryptedInput;
+      documentInput = documentEncryptedInput = new ByteArrayDataInput(); // empty input by default
+
+      // Read non-encrypted document data only if it exists
+      if (hasUnencrypted) {
+        int offset = offsets[index];
+        int totalLength = offsets[chunkDocs];
+        length = offsets[index + 1] - offset;
+        numStoredFields = this.numStoredFields[index];
+        documentInput = this.readDoc(startPointer, sliced, offset, length, totalLength, bytes, decompressor);
+      }
+
+      // Read encrypted document data only if it exists
+      if (hasEncrypted) {
+        int offsetEncrypted = offsetsEncrypted[index];
+        int totalLengthEncrypted = offsetsEncrypted[chunkDocs];
+        lengthEncrypted = offsetsEncrypted[index + 1] - offsetEncrypted;
+        numEncryptedStoredFields = this.numEncryptedStoredFields[index];
+        documentEncryptedInput = this.readDoc(startPointerEncrypted, slicedEncrypted, offsetEncrypted,
+            lengthEncrypted, totalLengthEncrypted, bytesEncrypted, decipherDecompressor);
+      }
+
+      return new SerializedDocument(documentInput, length, numStoredFields, documentEncryptedInput, lengthEncrypted, numEncryptedStoredFields);
+    }
+
+    private DataInput readDoc(long startPointer, boolean sliced, int offset, int length, int totalLength, BytesRef bytes, Decompressor decompressor)
+    throws IOException {
+      if (length == 0) {
+        // empty
+        return new ByteArrayDataInput();
+      } else if (merging) {
+        // already decompressed
+        return new ByteArrayDataInput(bytes.bytes, bytes.offset + offset, length);
+      } else if (sliced) {
+        fieldsStream.seek(startPointer);
+        decompressor.decompress(fieldsStream, chunkSize, offset, Math.min(length, chunkSize - offset), bytes);
+        return new DataInput() {
+
+          int decompressed = bytes.length;
+
+          void fillBuffer() throws IOException {
+            assert decompressed <= length;
+            if (decompressed == length) {
+              throw new EOFException();
+            }
+            final int toDecompress = Math.min(length - decompressed, chunkSize);
+            decompressor.decompress(fieldsStream, toDecompress, 0, toDecompress, bytes);
+            decompressed += toDecompress;
+          }
+
+          @Override
+          public byte readByte() throws IOException {
+            if (bytes.length == 0) {
+              fillBuffer();
+            }
+            --bytes.length;
+            return bytes.bytes[bytes.offset++];
+          }
+
+          @Override
+          public void readBytes(byte[] b, int offset, int len) throws IOException {
+            while (len > bytes.length) {
+              System.arraycopy(bytes.bytes, bytes.offset, b, offset, bytes.length);
+              len -= bytes.length;
+              offset += bytes.length;
+              fillBuffer();
+            }
+            System.arraycopy(bytes.bytes, bytes.offset, b, offset, len);
+            bytes.offset += len;
+            bytes.length -= len;
+          }
+
+        };
+      } else {
+        fieldsStream.seek(startPointer);
+        decompressor.decompress(fieldsStream, totalLength, offset, length, bytes);
+        assert bytes.length == length;
+        return new ByteArrayDataInput(bytes.bytes, bytes.offset, bytes.length);
+      }
+    }
+
+  }
+
+  SerializedDocument document(int docID) throws IOException {
+    if (state.contains(docID) == false) {
+      fieldsStream.seek(indexReader.getStartPointer(docID));
+      state.reset(docID);
+    }
+    assert state.contains(docID);
+    return state.document(docID);
+  }
+
+  @Override
+  public void visitDocument(int docID, StoredFieldVisitor visitor)
+      throws IOException {
+
+    final SerializedDocument doc = document(docID);
+
+    // visit non-encrypted stored fields only if they exist
+    if (doc.length > 0) {
+      this.visitStoredFields(visitor, doc.numStoredFields, doc.in);
+    }
+    // visit encrypted stored fields only if they exist
+    if (doc.encryptedLength > 0) {
+      this.visitStoredFields(visitor, doc.numEncryptedStoredFields, doc.encryptedIn);
+    }
+  }
+
+  private void visitStoredFields(StoredFieldVisitor visitor, int numStoredFields, DataInput in) throws IOException {
+    for (int fieldIDX = 0; fieldIDX < numStoredFields; fieldIDX++) {
+      final long infoAndBits = in.readVLong();
+      final int fieldNumber = (int) (infoAndBits >>> EncryptedCompressingStoredFieldsWriter.TYPE_BITS);
+      final FieldInfo fieldInfo = fieldInfos.fieldInfo(fieldNumber);
+
+      final int bits = (int) (infoAndBits & EncryptedCompressingStoredFieldsWriter.TYPE_MASK);
+      assert bits <= EncryptedCompressingStoredFieldsWriter.NUMERIC_DOUBLE: "bits=" + Integer.toHexString(bits);
+
+      switch(visitor.needsField(fieldInfo)) {
+        case YES:
+          readField(in, visitor, fieldInfo, bits);
+          break;
+        case NO:
+          if (fieldIDX == numStoredFields - 1) {// don't skipField on last field value; treat like STOP
+            return;
+          }
+          skipField(in, bits);
+          break;
+        case STOP:
+          return;
+      }
+    }
+  }
+
+  @Override
+  public StoredFieldsReader clone() {
+    ensureOpen();
+    return new EncryptedCompressingStoredFieldsReader(this, false);
+  }
+
+  @Override
+  public StoredFieldsReader getMergeInstance() {
+    ensureOpen();
+    return new EncryptedCompressingStoredFieldsReader(this, true);
+  }
+
+  int getVersion() {
+    return version;
+  }
+
+  long getCipherVersion() {
+    return cipherVersion;
+  }
+
+  CompressionMode getCompressionMode() {
+    return compressionMode;
+  }
+  
+  CompressingStoredFieldsIndexReader getIndexReader() {
+    return indexReader;
+  }
+  
+  long getMaxPointer() {
+    return maxPointer;
+  }
+  
+  IndexInput getFieldsStream() {
+    return fieldsStream;
+  }
+
+  int getChunkSize() {
+    return chunkSize;
+  }
+  
+  long getNumChunks() {
+    return numChunks;
+  }
+  
+  long getNumDirtyChunks() {
+    return numDirtyChunks;
+  }
+
+  int getPackedIntsVersion() {
+    return packedIntsVersion;
+  }
+
+  @Override
+  public long ramBytesUsed() {
+    return indexReader.ramBytesUsed();
+  }
+  
+  @Override
+  public Collection<Accountable> getChildResources() {
+    return Collections.singleton(Accountables.namedAccountable("stored field index", indexReader));
+  }
+
+  @Override
+  public void checkIntegrity() throws IOException {
+    CodecUtil.checksumEntireFile(fieldsStream);
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + "(mode=" + compressionMode + ",chunksize=" + chunkSize + ")";
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingStoredFieldsWriter.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingStoredFieldsWriter.java
new file mode 100644
index 0000000..7124aa4
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingStoredFieldsWriter.java
@@ -0,0 +1,756 @@
+package org.apache.lucene.codecs.encrypted.compressing;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.CodecUtil;
+import org.apache.lucene.codecs.StoredFieldsReader;
+import org.apache.lucene.codecs.StoredFieldsWriter;
+import org.apache.lucene.codecs.compressing.CompressionMode;
+import org.apache.lucene.codecs.compressing.Compressor;
+import org.apache.lucene.codecs.compressing.GrowableByteArrayDataOutput;
+import org.apache.lucene.codecs.encrypted.CipherFactory;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.FieldInfo;
+import org.apache.lucene.index.FieldInfos;
+import org.apache.lucene.index.IndexFileNames;
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.index.MergeState;
+import org.apache.lucene.index.SegmentInfo;
+import org.apache.lucene.store.DataOutput;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.IOContext;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BitUtil;
+import org.apache.lucene.util.Bits;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.IOUtils;
+import org.apache.lucene.util.packed.PackedInts;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * {@link StoredFieldsWriter} impl for {@link EncryptedCompressingStoredFieldsFormat}.
+ * @lucene.experimental
+ */
+public final class EncryptedCompressingStoredFieldsWriter extends StoredFieldsWriter {
+
+  /** Extension of stored fields file */
+  public static final String FIELDS_EXTENSION = "fdt";
+
+  /** Extension of stored fields index file */
+  public static final String FIELDS_INDEX_EXTENSION = "fdx";
+
+  static final int         STRING = 0x00;
+  static final int       BYTE_ARR = 0x01;
+  static final int    NUMERIC_INT = 0x02;
+  static final int  NUMERIC_FLOAT = 0x03;
+  static final int   NUMERIC_LONG = 0x04;
+  static final int NUMERIC_DOUBLE = 0x05;
+
+  static final int TYPE_BITS = PackedInts.bitsRequired(NUMERIC_DOUBLE);
+  static final int TYPE_MASK = (int) PackedInts.maxValue(TYPE_BITS);
+
+  static final String CODEC_SFX_IDX = "Index";
+  static final String CODEC_SFX_DAT = "Data";
+  static final int VERSION_START = 0;
+  static final int VERSION_CHUNK_STATS = 1;
+  static final int VERSION_CURRENT = VERSION_CHUNK_STATS;
+
+  private final String segment;
+  private CompressingStoredFieldsIndexWriter indexWriter;
+  private IndexOutput fieldsStream;
+
+  private final Compressor compressor;
+  private final CompressionMode compressionMode;
+  private final int chunkSize;
+  private final int maxDocsPerChunk;
+
+  private final GrowableByteArrayDataOutput bufferedEncryptedDocs;
+  private int[] numEncryptedStoredFields; // number of encrypted stored fields
+  private int[] endEncryptedOffsets; // end offsets in bufferedEncryptedDocs
+  private final GrowableByteArrayDataOutput bufferedDocs;
+  private int[] numStoredFields; // number of stored fields
+  private int[] endOffsets; // end offsets in bufferedDocs
+  private int docBase; // doc ID at the beginning of the chunk
+  private int numBufferedDocs; // docBase + numBufferedDocs == current doc ID
+
+  private long numChunks; // number of compressed blocks written
+  private long numDirtyChunks; // number of incomplete compressed blocks written
+
+  private final GrowableByteArrayDataOutput bufferedCompressedBlock;
+  private final Compressor encipherCompressor;
+  private long encipherVersion;
+  private EncryptedCompressingStoredFieldsFormat parentFormat;
+
+  /** Sole constructor. */
+  public EncryptedCompressingStoredFieldsWriter(EncryptedCompressingStoredFieldsFormat parentFormat,
+                                                Directory directory, SegmentInfo si,
+                                                String segmentSuffix, IOContext context, String formatName,
+                                                CompressionMode compressionMode, int chunkSize, int maxDocsPerChunk,
+                                                int blockSize) throws IOException {
+    assert directory != null;
+    this.parentFormat = parentFormat;
+    this.encipherVersion = parentFormat.getCipherFactory().getEncipherVersion();
+    this.segment = si.name;
+    this.compressionMode = compressionMode;
+    this.compressor = compressionMode.newCompressor();
+    this.encipherCompressor = new EncipherCompressor(parentFormat.getCipherFactory().newEncipherInstance(), compressor);
+    this.chunkSize = chunkSize;
+    this.maxDocsPerChunk = maxDocsPerChunk;
+    this.docBase = 0;
+    this.bufferedDocs = new GrowableByteArrayDataOutput(chunkSize);
+    this.numStoredFields = new int[16];
+    this.endOffsets = new int[16];
+    this.bufferedEncryptedDocs = new GrowableByteArrayDataOutput(chunkSize);
+    this.numEncryptedStoredFields = new int[16];
+    this.endEncryptedOffsets = new int[16];
+    this.numBufferedDocs = 0;
+    this.bufferedCompressedBlock = new GrowableByteArrayDataOutput(chunkSize);
+
+    boolean success = false;
+    IndexOutput indexStream = directory.createOutput(IndexFileNames.segmentFileName(segment, segmentSuffix, FIELDS_INDEX_EXTENSION), 
+                                                                     context);
+    try {
+      fieldsStream = directory.createOutput(IndexFileNames.segmentFileName(segment, segmentSuffix, FIELDS_EXTENSION),
+                                                    context);
+
+      final String codecNameIdx = formatName + CODEC_SFX_IDX;
+      final String codecNameDat = formatName + CODEC_SFX_DAT;
+      CodecUtil.writeIndexHeader(indexStream, codecNameIdx, VERSION_CURRENT, si.getId(), segmentSuffix);
+      CodecUtil.writeIndexHeader(fieldsStream, codecNameDat, VERSION_CURRENT, si.getId(), segmentSuffix);
+      assert CodecUtil.indexHeaderLength(codecNameDat, segmentSuffix) == fieldsStream.getFilePointer();
+      assert CodecUtil.indexHeaderLength(codecNameIdx, segmentSuffix) == indexStream.getFilePointer();
+
+      indexWriter = new CompressingStoredFieldsIndexWriter(indexStream, blockSize);
+      indexStream = null;
+
+      fieldsStream.writeVInt(chunkSize);
+      fieldsStream.writeVInt(PackedInts.VERSION_CURRENT);
+
+      success = true;
+    } finally {
+      if (!success) {
+        IOUtils.closeWhileHandlingException(fieldsStream, indexStream, indexWriter);
+      }
+    }
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      IOUtils.close(fieldsStream, indexWriter);
+    } finally {
+      fieldsStream = null;
+      indexWriter = null;
+    }
+  }
+
+  private int numStoredFieldsInDoc;
+  private int numEncryptedStoredFieldsInDoc;
+
+  @Override
+  public void startDocument() throws IOException {
+  }
+
+  @Override
+  public void finishDocument() throws IOException {
+    if (numBufferedDocs == this.numStoredFields.length) {
+      final int newLength = ArrayUtil.oversize(numBufferedDocs + 1, 4);
+      this.numStoredFields = Arrays.copyOf(this.numStoredFields, newLength);
+      endOffsets = Arrays.copyOf(endOffsets, newLength);
+      this.numEncryptedStoredFields = Arrays.copyOf(this.numEncryptedStoredFields, newLength);
+      endEncryptedOffsets = Arrays.copyOf(endEncryptedOffsets, newLength);
+    }
+
+    this.numStoredFields[numBufferedDocs] = numStoredFieldsInDoc;
+    numStoredFieldsInDoc = 0;
+    this.numEncryptedStoredFields[numBufferedDocs] = numEncryptedStoredFieldsInDoc;
+    numEncryptedStoredFieldsInDoc = 0;
+
+    endOffsets[numBufferedDocs] = bufferedDocs.length;
+    endEncryptedOffsets[numBufferedDocs] = bufferedEncryptedDocs.length;
+
+    ++numBufferedDocs;
+    if (triggerFlush()) {
+      flush();
+    }
+  }
+
+  private static void saveInts(int[] values, int length, DataOutput out) throws IOException {
+    assert length > 0;
+    if (length == 1) {
+      out.writeVInt(values[0]);
+    } else {
+      boolean allEqual = true;
+      for (int i = 1; i < length; ++i) {
+        if (values[i] != values[0]) {
+          allEqual = false;
+          break;
+        }
+      }
+      if (allEqual) {
+        out.writeVInt(0);
+        out.writeVInt(values[0]);
+      } else {
+        long max = 0;
+        for (int i = 0; i < length; ++i) {
+          max |= values[i];
+        }
+        final int bitsRequired = PackedInts.bitsRequired(max);
+        out.writeVInt(bitsRequired);
+        final PackedInts.Writer w = PackedInts.getWriterNoHeader(out, PackedInts.Format.PACKED, length, bitsRequired, 1);
+        for (int i = 0; i < length; ++i) {
+          w.add(values[i]);
+        }
+        w.finish();
+      }
+    }
+  }
+
+  private void writeHeader(int docBase, int numBufferedDocs,
+                           boolean hasUnencrypted, int[] numStoredFields, int[] lengths, boolean sliced,
+                           boolean hasEncrypted, int[] numEncryptedStoredFields, int[] lengthsEncrypted, boolean slicedEncrypted)
+  throws IOException {
+    int slicedBits = (sliced ? 1 : 0) << 1;
+    slicedBits = slicedBits ^ (slicedEncrypted ? 1 : 0);
+
+    int encryptedBits = (hasUnencrypted ? 1 : 0) << 1;
+    encryptedBits = encryptedBits ^ (hasEncrypted ? 1 : 0);
+
+    // save docBase and numBufferedDocs
+    fieldsStream.writeVInt(docBase);
+    fieldsStream.writeVLong((numBufferedDocs) << 4 | slicedBits << 2 | encryptedBits);
+
+    // save numStoredFields and lengths only if necessary
+    if (hasUnencrypted) {
+      saveInts(numStoredFields, numBufferedDocs, fieldsStream);
+      saveInts(lengths, numBufferedDocs, fieldsStream);
+    }
+
+    // save numEncryptedStoredFields and lengthsEncrypted only if necessary
+    if (hasEncrypted) {
+      saveInts(numEncryptedStoredFields, numBufferedDocs, fieldsStream);
+      saveInts(lengthsEncrypted, numBufferedDocs, fieldsStream);
+    }
+  }
+
+  private boolean triggerFlush() {
+    return bufferedDocs.length >= chunkSize || bufferedEncryptedDocs.length >= chunkSize || // chunks of at least chunkSize bytes
+        numBufferedDocs >= maxDocsPerChunk;
+  }
+
+  private void flush() throws IOException {
+    indexWriter.writeIndex(numBufferedDocs, fieldsStream.getFilePointer());
+
+    // transform end offsets into lengths
+    final int[] lengths = this.toLengths(endOffsets);
+    final int[] lengthsEncrypted = this.toLengths(endEncryptedOffsets);
+
+    final boolean sliced = bufferedDocs.length >= 2 * chunkSize;
+    final boolean slicedEncrypted = bufferedEncryptedDocs.length >= 2 * chunkSize;
+
+    final boolean hasUnencrypted = bufferedDocs.length > 0;
+    final boolean hasEncrypted = bufferedEncryptedDocs.length > 0;
+
+    writeHeader(docBase, numBufferedDocs,
+        hasUnencrypted, numStoredFields, lengths, sliced,
+        hasEncrypted, numEncryptedStoredFields, lengthsEncrypted, slicedEncrypted);
+
+    // we need an intermediate buffer to for the compressed block in order to
+    // store the length of the compressed block for the non-encrypted stored fields.
+    // todo: how to avoid this ?
+
+    // reset intermediate buffer
+    bufferedCompressedBlock.length = 0;
+
+    // compress stored fields to bufferedCompressedBlock only if it exists
+    if (hasUnencrypted) {
+      if (sliced) {
+        // big chunk, slice it
+        for (int compressed = 0; compressed < bufferedDocs.length; compressed += chunkSize) {
+          compressor.compress(bufferedDocs.bytes, compressed, Math.min(chunkSize, bufferedDocs.length - compressed), bufferedCompressedBlock);
+        }
+      } else {
+        compressor.compress(bufferedDocs.bytes, 0, bufferedDocs.length, bufferedCompressedBlock);
+      }
+    }
+
+    int unencryptedLength = bufferedCompressedBlock.length;
+
+    // compress and encrypt stored fields to bufferedCompressedBlock only if it exists
+    if (hasEncrypted) {
+      if (slicedEncrypted) {
+        // big chunk, slice it
+        for (int compressed = 0; compressed < bufferedEncryptedDocs.length; compressed += chunkSize) {
+          encipherCompressor.compress(bufferedEncryptedDocs.bytes, compressed, Math.min(chunkSize, bufferedEncryptedDocs.length - compressed), bufferedCompressedBlock);
+        }
+      } else {
+        encipherCompressor.compress(bufferedEncryptedDocs.bytes, 0, bufferedEncryptedDocs.length, bufferedCompressedBlock);
+      }
+    }
+
+    // write the length of the non-encrypted compressed block
+    fieldsStream.writeVInt(unencryptedLength);
+
+    // write the encrypted compressed block
+    fieldsStream.writeBytes(bufferedCompressedBlock.bytes, 0, bufferedCompressedBlock.length);
+
+    // reset
+    docBase += numBufferedDocs;
+    numBufferedDocs = 0;
+    bufferedDocs.length = 0;
+    bufferedEncryptedDocs.length = 0;
+    numChunks++;
+  }
+
+  /**
+   * Transform end offsets into lengths
+   */
+  private int[] toLengths(int[] endOffsets) {
+    final int[] lengths = endOffsets;
+    for (int i = numBufferedDocs - 1; i > 0; --i) {
+      lengths[i] = endOffsets[i] - endOffsets[i - 1];
+      assert lengths[i] >= 0;
+    }
+    return lengths;
+  }
+  
+  @Override
+  public void writeField(FieldInfo info, IndexableField field) throws IOException {
+    if (!parentFormat.isFieldEncrypted(info.name)) {
+      ++numStoredFieldsInDoc;
+      this.writeField(bufferedDocs, info, field);
+    }
+    else {
+      ++numEncryptedStoredFieldsInDoc;
+      this.writeField(bufferedEncryptedDocs, info, field);
+    }
+  }
+
+  private void writeField(DataOutput out, FieldInfo info, IndexableField field)  throws IOException {
+    int bits = 0;
+    final BytesRef bytes;
+    final String string;
+
+    Number number = field.numericValue();
+    if (number != null) {
+      if (number instanceof Byte || number instanceof Short || number instanceof Integer) {
+        bits = NUMERIC_INT;
+      } else if (number instanceof Long) {
+        bits = NUMERIC_LONG;
+      } else if (number instanceof Float) {
+        bits = NUMERIC_FLOAT;
+      } else if (number instanceof Double) {
+        bits = NUMERIC_DOUBLE;
+      } else {
+        throw new IllegalArgumentException("cannot store numeric type " + number.getClass());
+      }
+      string = null;
+      bytes = null;
+    } else {
+      bytes = field.binaryValue();
+      if (bytes != null) {
+        bits = BYTE_ARR;
+        string = null;
+      } else {
+        bits = STRING;
+        string = field.stringValue();
+        if (string == null) {
+          throw new IllegalArgumentException("field " + field.name() + " is stored but does not have binaryValue, stringValue nor numericValue");
+        }
+      }
+    }
+
+    final long infoAndBits = (((long) info.number) << TYPE_BITS) | bits;
+    out.writeVLong(infoAndBits);
+
+    if (bytes != null) {
+      out.writeVInt(bytes.length);
+      out.writeBytes(bytes.bytes, bytes.offset, bytes.length);
+    } else if (string != null) {
+      out.writeString(string);
+    } else {
+      if (number instanceof Byte || number instanceof Short || number instanceof Integer) {
+        out.writeZInt(number.intValue());
+      } else if (number instanceof Long) {
+        writeTLong(out, number.longValue());
+      } else if (number instanceof Float) {
+        writeZFloat(out, number.floatValue());
+      } else if (number instanceof Double) {
+        writeZDouble(out, number.doubleValue());
+      } else {
+        throw new AssertionError("Cannot get here");
+      }
+    }
+  }
+
+  // -0 isn't compressed.
+  static final int NEGATIVE_ZERO_FLOAT = Float.floatToIntBits(-0f);
+  static final long NEGATIVE_ZERO_DOUBLE = Double.doubleToLongBits(-0d);
+
+  // for compression of timestamps
+  static final long SECOND = 1000L;
+  static final long HOUR = 60 * 60 * SECOND;
+  static final long DAY = 24 * HOUR;
+  static final int SECOND_ENCODING = 0x40;
+  static final int HOUR_ENCODING = 0x80;
+  static final int DAY_ENCODING = 0xC0;
+
+  /** 
+   * Writes a float in a variable-length format.  Writes between one and 
+   * five bytes. Small integral values typically take fewer bytes.
+   * <p>
+   * ZFloat --&gt; Header, Bytes*?
+   * <ul>
+   *    <li>Header --&gt; {@link DataOutput#writeByte Uint8}. When it is
+   *       equal to 0xFF then the value is negative and stored in the next
+   *       4 bytes. Otherwise if the first bit is set then the other bits
+   *       in the header encode the value plus one and no other
+   *       bytes are read. Otherwise, the value is a positive float value
+   *       whose first byte is the header, and 3 bytes need to be read to
+   *       complete it.
+   *    <li>Bytes --&gt; Potential additional bytes to read depending on the
+   *       header.
+   * </ul>
+   */
+  static void writeZFloat(DataOutput out, float f) throws IOException {
+    int intVal = (int) f;
+    final int floatBits = Float.floatToIntBits(f);
+
+    if (f == intVal
+        && intVal >= -1
+        && intVal <= 0x7D
+        && floatBits != NEGATIVE_ZERO_FLOAT) {
+      // small integer value [-1..125]: single byte
+      out.writeByte((byte) (0x80 | (1 + intVal)));
+    } else if ((floatBits >>> 31) == 0) {
+      // other positive floats: 4 bytes
+      out.writeInt(floatBits);
+    } else {
+      // other negative float: 5 bytes
+      out.writeByte((byte) 0xFF);
+      out.writeInt(floatBits);
+    }
+  }
+  
+  /** 
+   * Writes a float in a variable-length format.  Writes between one and 
+   * five bytes. Small integral values typically take fewer bytes.
+   * <p>
+   * ZFloat --&gt; Header, Bytes*?
+   * <ul>
+   *    <li>Header --&gt; {@link DataOutput#writeByte Uint8}. When it is
+   *       equal to 0xFF then the value is negative and stored in the next
+   *       8 bytes. When it is equal to 0xFE then the value is stored as a
+   *       float in the next 4 bytes. Otherwise if the first bit is set
+   *       then the other bits in the header encode the value plus one and
+   *       no other bytes are read. Otherwise, the value is a positive float
+   *       value whose first byte is the header, and 7 bytes need to be read
+   *       to complete it.
+   *    <li>Bytes --&gt; Potential additional bytes to read depending on the
+   *       header.
+   * </ul>
+   */
+  static void writeZDouble(DataOutput out, double d) throws IOException {
+    int intVal = (int) d;
+    final long doubleBits = Double.doubleToLongBits(d);
+    
+    if (d == intVal &&
+        intVal >= -1 && 
+        intVal <= 0x7C &&
+        doubleBits != NEGATIVE_ZERO_DOUBLE) {
+      // small integer value [-1..124]: single byte
+      out.writeByte((byte) (0x80 | (intVal + 1)));
+      return;
+    } else if (d == (float) d) {
+      // d has an accurate float representation: 5 bytes
+      out.writeByte((byte) 0xFE);
+      out.writeInt(Float.floatToIntBits((float) d));
+    } else if ((doubleBits >>> 63) == 0) {
+      // other positive doubles: 8 bytes
+      out.writeLong(doubleBits);
+    } else {
+      // other negative doubles: 9 bytes
+      out.writeByte((byte) 0xFF);
+      out.writeLong(doubleBits);
+    }
+  }
+
+  /** 
+   * Writes a long in a variable-length format.  Writes between one and 
+   * ten bytes. Small values or values representing timestamps with day,
+   * hour or second precision typically require fewer bytes.
+   * <p>
+   * ZLong --&gt; Header, Bytes*?
+   * <ul>
+   *    <li>Header --&gt; The first two bits indicate the compression scheme:
+   *       <ul>
+   *          <li>00 - uncompressed
+   *          <li>01 - multiple of 1000 (second)
+   *          <li>10 - multiple of 3600000 (hour)
+   *          <li>11 - multiple of 86400000 (day)
+   *       </ul>
+   *       Then the next bit is a continuation bit, indicating whether more
+   *       bytes need to be read, and the last 5 bits are the lower bits of
+   *       the encoded value. In order to reconstruct the value, you need to
+   *       combine the 5 lower bits of the header with a vLong in the next
+   *       bytes (if the continuation bit is set to 1). Then
+   *       {@link BitUtil#zigZagDecode(int) zigzag-decode} it and finally
+   *       multiply by the multiple corresponding to the compression scheme.
+   *    <li>Bytes --&gt; Potential additional bytes to read depending on the
+   *       header.
+   * </ul>
+   */
+  // T for "timestamp"
+  static void writeTLong(DataOutput out, long l) throws IOException {
+    int header; 
+    if (l % SECOND != 0) {
+      header = 0;
+    } else if (l % DAY == 0) {
+      // timestamp with day precision
+      header = DAY_ENCODING;
+      l /= DAY;
+    } else if (l % HOUR == 0) {
+      // timestamp with hour precision, or day precision with a timezone
+      header = HOUR_ENCODING;
+      l /= HOUR;
+    } else {
+      // timestamp with second precision
+      header = SECOND_ENCODING;
+      l /= SECOND;
+    }
+
+    final long zigZagL = BitUtil.zigZagEncode(l);
+    header |= (zigZagL & 0x1F); // last 5 bits
+    final long upperBits = zigZagL >>> 5;
+    if (upperBits != 0) {
+      header |= 0x20;
+    }
+    out.writeByte((byte) header);
+    if (upperBits != 0) {
+      out.writeVLong(upperBits);
+    }
+  }
+
+  @Override
+  public void finish(FieldInfos fis, int numDocs) throws IOException {
+    if (numBufferedDocs > 0) {
+      flush();
+      numDirtyChunks++; // incomplete: we had to force this flush
+    } else {
+      assert bufferedDocs.length == 0;
+      assert bufferedEncryptedDocs.length == 0;
+    }
+    if (docBase != numDocs) {
+      throw new RuntimeException("Wrote " + docBase + " docs, finish called with numDocs=" + numDocs);
+    }
+    // Ensure that the encrypted attribute is added to the field infos
+    for (FieldInfo fi : fis) {
+      if (parentFormat.isFieldEncrypted(fi.name)) {
+        fi.putAttribute(CipherFactory.ENCRYPTED_FIELD_KEY, Boolean.toString(true));
+      }
+    }
+    indexWriter.finish(numDocs, fieldsStream.getFilePointer());
+    fieldsStream.writeVLong(numChunks);
+    fieldsStream.writeVLong(numDirtyChunks);
+    CodecUtil.writeFooter(fieldsStream);
+    assert bufferedDocs.length == 0;
+    assert bufferedEncryptedDocs.length == 0;
+  }
+  
+  // bulk merge is scary: its caused corruption bugs in the past.
+  // we try to be extra safe with this impl, but add an escape hatch to
+  // have a workaround for undiscovered bugs.
+  static final String BULK_MERGE_ENABLED_SYSPROP = EncryptedCompressingStoredFieldsWriter.class.getName() + ".enableBulkMerge";
+  static final boolean BULK_MERGE_ENABLED;
+  static {
+    boolean v = true;
+    try {
+      v = Boolean.parseBoolean(System.getProperty(BULK_MERGE_ENABLED_SYSPROP, "true"));
+    } catch (SecurityException ignored) {}
+    BULK_MERGE_ENABLED = v;
+  }
+
+  @Override
+  public int merge(MergeState mergeState) throws IOException {
+    int docCount = 0;
+    int numReaders = mergeState.maxDocs.length;
+
+    // Add the encrypted attribute to the merged field infos (they will be used by the
+    // EncryptedMatchingReaders).
+    for (FieldInfo mergeFi : mergeState.mergeFieldInfos) {
+      if (parentFormat.isFieldEncrypted(mergeFi.name)) {
+        mergeFi.putAttribute(CipherFactory.ENCRYPTED_FIELD_KEY, Boolean.toString(true));
+      }
+    }
+
+    // we can only bulk-copy documents (second case below)
+    // if the matching reader is also an EncryptedCompressingStoredFieldsReader,
+    // and if the matching reader has the same set or a subset of the encrypted fields,
+    // and if the cipher version is the same.
+    //
+    // we can bulk-copy stored fields on a per document basis (third case below)
+    // if the matching reader is also an EncryptedCompressingStoredFieldsReader,
+    // and if the matching reader has the same set or a subset of the encrypted fields.
+    //
+    // otherwise we need to use the naive merge (first case below)
+
+    // Matching readers will check that the reader is an EncryptedCompressingStoredFieldsReader
+    // and that it has the same set or a subset of the encrypted fields of the merged field infos.
+    EncryptedMatchingReaders matching = new EncryptedMatchingReaders(mergeState);
+
+    for (int readerIndex = 0; readerIndex < numReaders; readerIndex++) {
+      MergeVisitor visitor = new MergeVisitor(mergeState, readerIndex);
+      EncryptedCompressingStoredFieldsReader matchingFieldsReader = null;
+      if (matching.matchingReaders[readerIndex]) {
+        final StoredFieldsReader fieldsReader = mergeState.storedFieldsReaders[readerIndex];
+        if (fieldsReader != null && fieldsReader instanceof EncryptedCompressingStoredFieldsReader) {
+          matchingFieldsReader = (EncryptedCompressingStoredFieldsReader) fieldsReader;
+        }
+      }
+
+      final int maxDoc = mergeState.maxDocs[readerIndex];
+      final Bits liveDocs = mergeState.liveDocs[readerIndex];
+
+      // if its some other format, or an older version of this format, or safety switch:
+      if (matchingFieldsReader == null || matchingFieldsReader.getVersion() != VERSION_CURRENT || BULK_MERGE_ENABLED == false) {
+        // naive merge...
+        StoredFieldsReader storedFieldsReader = mergeState.storedFieldsReaders[readerIndex];
+        if (storedFieldsReader != null) {
+          storedFieldsReader.checkIntegrity();
+        }
+        for (int docID = 0; docID < maxDoc; docID++) {
+          if (liveDocs != null && liveDocs.get(docID) == false) {
+            continue;
+          }
+          startDocument();
+          storedFieldsReader.visitDocument(docID, visitor);
+          finishDocument();
+          ++docCount;
+        }
+      } else if (matchingFieldsReader.getCompressionMode() == compressionMode &&
+          matchingFieldsReader.getChunkSize() == chunkSize &&
+          matchingFieldsReader.getPackedIntsVersion() == PackedInts.VERSION_CURRENT &&
+          matchingFieldsReader.getCipherVersion() == encipherVersion &&
+          liveDocs == null &&
+          !tooDirty(matchingFieldsReader)) {
+        // optimized merge, raw byte copy
+        // its not worth fine-graining this if there are deletions.
+
+        // if the format is older, its always handled by the naive merge case above
+        assert matchingFieldsReader.getVersion() == VERSION_CURRENT;
+        // if the cipher version is older, its always handled by the naive merge case
+        assert matchingFieldsReader.getCipherVersion() == encipherVersion;
+        matchingFieldsReader.checkIntegrity();
+
+        // flush any pending chunks
+        if (numBufferedDocs > 0) {
+          flush();
+          numDirtyChunks++; // incomplete: we had to force this flush
+        }
+
+        // iterate over each chunk. we use the stored fields index to find chunk boundaries,
+        // read the docstart + doccount from the chunk header (we write a new header, since doc numbers will change),
+        // and just copy the bytes directly.
+        IndexInput rawDocs = matchingFieldsReader.getFieldsStream();
+        CompressingStoredFieldsIndexReader index = matchingFieldsReader.getIndexReader();
+        rawDocs.seek(index.getStartPointer(0));
+        int docID = 0;
+        while (docID < maxDoc) {
+          // read header
+          int base = rawDocs.readVInt();
+          if (base != docID) {
+            throw new CorruptIndexException("invalid state: base=" + base + ", docID=" + docID, rawDocs);
+          }
+          long code = rawDocs.readVLong();
+
+          // write a new index entry and new header for this chunk.
+          int bufferedDocs = (int) code >>> 4;
+          indexWriter.writeIndex(bufferedDocs, fieldsStream.getFilePointer());
+          fieldsStream.writeVInt(docBase); // rebase
+          fieldsStream.writeVLong(code);
+          docID += bufferedDocs;
+          docBase += bufferedDocs;
+          docCount += bufferedDocs;
+
+          if (docID > maxDoc) {
+            throw new CorruptIndexException("invalid state: base=" + base + ", count=" + bufferedDocs + ", maxDoc=" + maxDoc, rawDocs);
+          }
+
+          // copy bytes until the next chunk boundary (or end of chunk data).
+          // using the stored fields index for this isn't the most efficient, but fast enough
+          // and is a source of redundancy for detecting bad things.
+          final long end;
+          if (docID == maxDoc) {
+            end = matchingFieldsReader.getMaxPointer();
+          } else {
+            end = index.getStartPointer(docID);
+          }
+          fieldsStream.copyBytes(rawDocs, end - rawDocs.getFilePointer());
+        }
+
+        if (rawDocs.getFilePointer() != matchingFieldsReader.getMaxPointer()) {
+          throw new CorruptIndexException("invalid state: pos=" + rawDocs.getFilePointer() + ", max=" + matchingFieldsReader.getMaxPointer(), rawDocs);
+        }
+
+        // since we bulk merged all chunks, we inherit any dirty ones from this segment.
+        numChunks += matchingFieldsReader.getNumChunks();
+        numDirtyChunks += matchingFieldsReader.getNumDirtyChunks();
+      } else {
+        // optimized merge, we copy serialized (but decompressed) bytes directly
+        // even on simple docs (1 stored field), it seems to help by about 20%
+
+        // if the format is older, its always handled by the naive merge case above
+        assert matchingFieldsReader.getVersion() == VERSION_CURRENT;
+        matchingFieldsReader.checkIntegrity();
+
+        for (int docID = 0; docID < maxDoc; docID++) {
+          if (liveDocs != null && liveDocs.get(docID) == false) {
+            continue;
+          }
+          EncryptedCompressingStoredFieldsReader.SerializedDocument doc = matchingFieldsReader.document(docID);
+          startDocument();
+          bufferedDocs.copyBytes(doc.in, doc.length);
+          bufferedEncryptedDocs.copyBytes(doc.encryptedIn, doc.encryptedLength);
+          numStoredFieldsInDoc = doc.numStoredFields;
+          numEncryptedStoredFieldsInDoc = doc.numEncryptedStoredFields;
+          finishDocument();
+          ++docCount;
+        }
+      }
+    }
+    finish(mergeState.mergeFieldInfos, docCount);
+    return docCount;
+  }
+  
+  /** 
+   * Returns true if we should recompress this reader, even though we could bulk merge compressed data 
+   * <p>
+   * The last chunk written for a segment is typically incomplete, so without recompressing,
+   * in some worst-case situations (e.g. frequent reopen with tiny flushes), over time the 
+   * compression ratio can degrade. This is a safety switch.
+   */
+  boolean tooDirty(EncryptedCompressingStoredFieldsReader candidate) {
+    // more than 1% dirty, or more than hard limit of 1024 dirty chunks
+    return candidate.getNumDirtyChunks() > 1024 || 
+           candidate.getNumDirtyChunks() * 100 > candidate.getNumChunks();
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingTermVectorsFormat.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingTermVectorsFormat.java
new file mode 100644
index 0000000..195a21e
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingTermVectorsFormat.java
@@ -0,0 +1,136 @@
+package org.apache.lucene.codecs.encrypted.compressing;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.CodecUtil;
+import org.apache.lucene.codecs.StoredFieldsFormat;
+import org.apache.lucene.codecs.TermVectorsFormat;
+import org.apache.lucene.codecs.TermVectorsReader;
+import org.apache.lucene.codecs.TermVectorsWriter;
+import org.apache.lucene.codecs.compressing.CompressionMode;
+import org.apache.lucene.codecs.encrypted.CipherFactory;
+import org.apache.lucene.index.FieldInfos;
+import org.apache.lucene.index.SegmentInfo;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.IOContext;
+
+import java.io.IOException;
+
+/**
+ * A {@link TermVectorsFormat} that compresses chunks of documents together in
+ * order to improve the compression ratio.
+ * @lucene.experimental
+ */
+public abstract class EncryptedCompressingTermVectorsFormat extends TermVectorsFormat {
+
+  private final String formatName;
+  private final String segmentSuffix;
+  private final CompressionMode compressionMode;
+  private final int chunkSize;
+  private final int blockSize;
+
+  /**
+   * Create a new {@link EncryptedCompressingTermVectorsFormat}.
+   * <p>
+   * <code>formatName</code> is the name of the format. This name will be used
+   * in the file formats to perform
+   * {@link CodecUtil#checkIndexHeader codec header checks}.
+   * <p>
+   * The <code>compressionMode</code> parameter allows you to choose between
+   * compression algorithms that have various compression and decompression
+   * speeds so that you can pick the one that best fits your indexing and
+   * searching throughput. You should never instantiate two
+   * {@link EncryptedCompressingTermVectorsFormat}s that have the same name but
+   * different {@link CompressionMode}s.
+   * <p>
+   * <code>chunkSize</code> is the minimum byte size of a chunk of documents.
+   * Higher values of <code>chunkSize</code> should improve the compression
+   * ratio but will require more memory at indexing time and might make document
+   * loading a little slower (depending on the size of your OS cache compared
+   * to the size of your index).
+   *
+   * @param formatName the name of the {@link StoredFieldsFormat}
+   * @param segmentSuffix a suffix to append to files created by this format
+   * @param compressionMode the {@link CompressionMode} to use
+   * @param chunkSize the minimum number of bytes of a single chunk of stored documents
+   * @param blockSize the number of chunks to store in an index block.
+   * @see CompressionMode
+   */
+  public EncryptedCompressingTermVectorsFormat(String formatName, String segmentSuffix,
+                                               CompressionMode compressionMode, int chunkSize, int blockSize) {
+    this.formatName = formatName;
+    this.segmentSuffix = segmentSuffix;
+    this.compressionMode = compressionMode;
+    if (chunkSize < 1) {
+      throw new IllegalArgumentException("chunkSize must be >= 1");
+    }
+    this.chunkSize = chunkSize;
+    if (blockSize < 1) {
+      throw new IllegalArgumentException("blockSize must be >= 1");
+    }
+    this.blockSize = blockSize;
+  }
+
+  @Override
+  public final TermVectorsReader vectorsReader(Directory directory,
+      SegmentInfo segmentInfo, FieldInfos fieldInfos, IOContext context)
+      throws IOException {
+    String value = segmentInfo.getAttribute(CipherFactory.CIPHER_VERSION_KEY);
+    if (value == null) {
+      throw new IllegalStateException("missing value for " + CipherFactory.CIPHER_VERSION_KEY +
+          " for segment: " + segmentInfo.name);
+    }
+
+    return new EncryptedCompressingTermVectorsReader(this.getCipherFactory(), directory, segmentInfo, segmentSuffix,
+        fieldInfos, context, formatName, compressionMode);
+  }
+
+  @Override
+  public final TermVectorsWriter vectorsWriter(Directory directory,
+      SegmentInfo segmentInfo, IOContext context) throws IOException {
+    // Store cipher version in segment info
+    long cipherVersion = this.getCipherFactory().getEncipherVersion();
+    // It might overwrite the value put by another format, e.g., EncryptedLucene50PostingsFormat
+    // We just check that it is the same version.
+    String previous = segmentInfo.putAttribute(CipherFactory.CIPHER_VERSION_KEY, Long.toString(cipherVersion));
+    if (previous != null && Long.parseLong(previous) != cipherVersion) {
+      throw new IllegalStateException("found existing value for " + CipherFactory.CIPHER_VERSION_KEY +
+          " for segment: " + segmentInfo.name + "old=" + previous + ", new=" + cipherVersion);
+    }
+
+    return new EncryptedCompressingTermVectorsWriter(this, directory, segmentInfo, segmentSuffix,
+        context, formatName, compressionMode, chunkSize, blockSize);
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + "(compressionMode=" + compressionMode
+        + ", chunkSize=" + chunkSize + ", blockSize=" + blockSize + ")";
+  }
+
+  /**
+   * Concrete implementation should specify the {@link CipherFactory}.
+   */
+  public abstract CipherFactory getCipherFactory();
+
+  /**
+   * Concrete implementation should specify which field to encrypt.
+   */
+  public abstract boolean isFieldEncrypted(String field);
+
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingTermVectorsReader.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingTermVectorsReader.java
new file mode 100644
index 0000000..461690e
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingTermVectorsReader.java
@@ -0,0 +1,1201 @@
+package org.apache.lucene.codecs.encrypted.compressing;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.CodecUtil;
+import org.apache.lucene.codecs.TermVectorsReader;
+import org.apache.lucene.codecs.compressing.CompressionMode;
+import org.apache.lucene.codecs.compressing.Decompressor;
+import org.apache.lucene.codecs.encrypted.CipherFactory;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.FieldInfo;
+import org.apache.lucene.index.FieldInfos;
+import org.apache.lucene.index.Fields;
+import org.apache.lucene.index.IndexFileNames;
+import org.apache.lucene.index.PostingsEnum;
+import org.apache.lucene.index.SegmentInfo;
+import org.apache.lucene.index.Terms;
+import org.apache.lucene.index.TermsEnum;
+import org.apache.lucene.store.AlreadyClosedException;
+import org.apache.lucene.store.ByteArrayDataInput;
+import org.apache.lucene.store.ChecksumIndexInput;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.IOContext;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.util.Accountable;
+import org.apache.lucene.util.Accountables;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.IOUtils;
+import org.apache.lucene.util.LongsRef;
+import org.apache.lucene.util.packed.BlockPackedReaderIterator;
+import org.apache.lucene.util.packed.PackedInts;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import static org.apache.lucene.codecs.encrypted.compressing.EncryptedCompressingTermVectorsWriter.CODEC_SFX_DAT;
+import static org.apache.lucene.codecs.encrypted.compressing.EncryptedCompressingTermVectorsWriter.CODEC_SFX_IDX;
+import static org.apache.lucene.codecs.encrypted.compressing.EncryptedCompressingTermVectorsWriter.FLAGS_BITS;
+import static org.apache.lucene.codecs.encrypted.compressing.EncryptedCompressingTermVectorsWriter.OFFSETS;
+import static org.apache.lucene.codecs.encrypted.compressing.EncryptedCompressingTermVectorsWriter.PACKED_BLOCK_SIZE;
+import static org.apache.lucene.codecs.encrypted.compressing.EncryptedCompressingTermVectorsWriter.PAYLOADS;
+import static org.apache.lucene.codecs.encrypted.compressing.EncryptedCompressingTermVectorsWriter.POSITIONS;
+import static org.apache.lucene.codecs.encrypted.compressing.EncryptedCompressingTermVectorsWriter.VECTORS_EXTENSION;
+import static org.apache.lucene.codecs.encrypted.compressing.EncryptedCompressingTermVectorsWriter.VECTORS_INDEX_EXTENSION;
+import static org.apache.lucene.codecs.encrypted.compressing.EncryptedCompressingTermVectorsWriter.VERSION_CHUNK_STATS;
+import static org.apache.lucene.codecs.encrypted.compressing.EncryptedCompressingTermVectorsWriter.VERSION_CURRENT;
+import static org.apache.lucene.codecs.encrypted.compressing.EncryptedCompressingTermVectorsWriter.VERSION_START;
+
+/**
+ * {@link TermVectorsReader} for {@link EncryptedCompressingTermVectorsFormat}.
+ * @lucene.experimental
+ */
+public final class EncryptedCompressingTermVectorsReader extends TermVectorsReader implements Closeable {
+
+  private final FieldInfos fieldInfos;
+  final CompressingStoredFieldsIndexReader indexReader;
+  final IndexInput vectorsStream;
+  private final int version;
+  private final long cipherVersion;
+  private final int packedIntsVersion;
+  private final CompressionMode compressionMode;
+  private final Decompressor decompressor;
+  private final Decompressor decipherDecompressor;
+  private final int chunkSize;
+  private final int numDocs;
+  private boolean closed;
+  private final BlockPackedReaderIterator reader;
+  private final long numChunks; // number of compressed blocks written
+  private final long numDirtyChunks; // number of incomplete compressed blocks written
+  private final long maxPointer; // end of the data section
+  private final CipherFactory cipherFactory;
+
+  // used by clone
+  private EncryptedCompressingTermVectorsReader(EncryptedCompressingTermVectorsReader reader) {
+    this.fieldInfos = reader.fieldInfos;
+    this.vectorsStream = reader.vectorsStream.clone();
+    this.indexReader = reader.indexReader.clone();
+    this.packedIntsVersion = reader.packedIntsVersion;
+    this.compressionMode = reader.compressionMode;
+    this.decompressor = reader.decompressor.clone();
+    this.cipherVersion = reader.cipherVersion;
+    this.cipherFactory = reader.cipherFactory;
+    this.decipherDecompressor = new DecipherDecompressor(this.cipherFactory, this.cipherVersion, this.decompressor);
+    this.chunkSize = reader.chunkSize;
+    this.numDocs = reader.numDocs;
+    this.reader = new BlockPackedReaderIterator(vectorsStream, packedIntsVersion, PACKED_BLOCK_SIZE, 0);
+    this.version = reader.version;
+    this.numChunks = reader.numChunks;
+    this.numDirtyChunks = reader.numDirtyChunks;
+    this.maxPointer = reader.maxPointer;
+    this.closed = false;
+  }
+
+  /** Sole constructor. */
+  public EncryptedCompressingTermVectorsReader(CipherFactory cipherFactory, Directory d, SegmentInfo si, String segmentSuffix, FieldInfos fn,
+                                               IOContext context, String formatName, CompressionMode compressionMode) throws IOException {
+    this.cipherFactory = cipherFactory;
+    this.compressionMode = compressionMode;
+    final String segment = si.name;
+    boolean success = false;
+    fieldInfos = fn;
+    numDocs = si.maxDoc();
+    int version = -1;
+    CompressingStoredFieldsIndexReader indexReader = null;
+
+    long maxPointer = -1;
+
+    // Load the index into memory
+    final String indexName = IndexFileNames.segmentFileName(segment, segmentSuffix, VECTORS_INDEX_EXTENSION);
+    try (ChecksumIndexInput input = d.openChecksumInput(indexName, context)) {
+      Throwable priorE = null;
+      try {
+        final String codecNameIdx = formatName + CODEC_SFX_IDX;
+        version = CodecUtil.checkIndexHeader(input, codecNameIdx, VERSION_START, VERSION_CURRENT, si.getId(), segmentSuffix);
+        assert CodecUtil.indexHeaderLength(codecNameIdx, segmentSuffix) == input.getFilePointer();
+        indexReader = new CompressingStoredFieldsIndexReader(input, si);
+        maxPointer = input.readVLong(); // the end of the data section
+      } catch (Throwable exception) {
+        priorE = exception;
+      } finally {
+        CodecUtil.checkFooter(input, priorE);
+      }
+    }
+    
+    this.version = version;
+    this.indexReader = indexReader;
+    this.maxPointer = maxPointer;
+
+    try {
+      // Open the data file and read metadata
+      final String vectorsStreamFN = IndexFileNames.segmentFileName(segment, segmentSuffix, VECTORS_EXTENSION);
+      vectorsStream = d.openInput(vectorsStreamFN, context);
+      final String codecNameDat = formatName + CODEC_SFX_DAT;
+      int version2 = CodecUtil.checkIndexHeader(vectorsStream, codecNameDat, VERSION_START, VERSION_CURRENT, si.getId(), segmentSuffix);
+      if (version != version2) {
+        throw new CorruptIndexException("Version mismatch between stored fields index and data: " + version + " != " + version2, vectorsStream);
+      }
+      assert CodecUtil.indexHeaderLength(codecNameDat, segmentSuffix) == vectorsStream.getFilePointer();
+      
+      long pos = vectorsStream.getFilePointer();
+      
+      if (version >= VERSION_CHUNK_STATS) {
+        vectorsStream.seek(maxPointer);
+        numChunks = vectorsStream.readVLong();
+        numDirtyChunks = vectorsStream.readVLong();
+        if (numDirtyChunks > numChunks) {
+          throw new CorruptIndexException("invalid chunk counts: dirty=" + numDirtyChunks + ", total=" + numChunks, vectorsStream);
+        }
+      } else {
+        numChunks = numDirtyChunks = -1;
+      }
+      
+      // NOTE: data file is too costly to verify checksum against all the bytes on open,
+      // but for now we at least verify proper structure of the checksum footer: which looks
+      // for FOOTER_MAGIC + algorithmID. This is cheap and can detect some forms of corruption
+      // such as file truncation.
+      CodecUtil.retrieveChecksum(vectorsStream);
+      vectorsStream.seek(pos);
+
+      packedIntsVersion = vectorsStream.readVInt();
+      chunkSize = vectorsStream.readVInt();
+      decompressor = compressionMode.newDecompressor();
+      this.reader = new BlockPackedReaderIterator(vectorsStream, packedIntsVersion, PACKED_BLOCK_SIZE, 0);
+
+      // Reads the cipher version from the segment info and instantiate the decipher decompressor
+      cipherVersion = Long.parseLong(si.getAttribute(CipherFactory.CIPHER_VERSION_KEY));
+      decipherDecompressor = new DecipherDecompressor(cipherFactory, cipherVersion, decompressor);
+
+      success = true;
+    } finally {
+      if (!success) {
+        IOUtils.closeWhileHandlingException(this);
+      }
+    }
+  }
+
+  CompressionMode getCompressionMode() {
+    return compressionMode;
+  }
+
+  int getChunkSize() {
+    return chunkSize;
+  }
+
+  int getPackedIntsVersion() {
+    return packedIntsVersion;
+  }
+  
+  int getVersion() {
+    return version;
+  }
+
+  long getCipherVersion() {
+    return cipherVersion;
+  }
+
+  CompressingStoredFieldsIndexReader getIndexReader() {
+    return indexReader;
+  }
+
+  IndexInput getVectorsStream() {
+    return vectorsStream;
+  }
+  
+  long getMaxPointer() {
+    return maxPointer;
+  }
+  
+  long getNumChunks() {
+    return numChunks;
+  }
+  
+  long getNumDirtyChunks() {
+    return numDirtyChunks;
+  }
+
+  /**
+   * @throws AlreadyClosedException if this TermVectorsReader is closed
+   */
+  private void ensureOpen() throws AlreadyClosedException {
+    if (closed) {
+      throw new AlreadyClosedException("this FieldsReader is closed");
+    }
+  }
+
+  @Override
+  public void close() throws IOException {
+    if (!closed) {
+      IOUtils.close(vectorsStream);
+      closed = true;
+    }
+  }
+
+  @Override
+  public TermVectorsReader clone() {
+    return new EncryptedCompressingTermVectorsReader(this);
+  }
+
+  @Override
+  public Fields get(int doc) throws IOException {
+    ensureOpen();
+
+    // seek to the right place
+    {
+      final long startPointer = indexReader.getStartPointer(doc);
+      vectorsStream.seek(startPointer);
+    }
+
+    // decode
+    // - docBase: first doc ID of the chunk
+    // - chunkDocs: number of docs of the chunk
+    final int docBase = vectorsStream.readVInt();
+    final int chunkDocs = vectorsStream.readVInt();
+    if (doc < docBase || doc >= docBase + chunkDocs || docBase + chunkDocs > numDocs) {
+      throw new CorruptIndexException("docBase=" + docBase + ",chunkDocs=" + chunkDocs + ",doc=" + doc, vectorsStream);
+    }
+
+    final int skip; // number of fields to skip
+    final int numFields; // number of fields of the document we're looking for
+    final int totalFields; // total number of fields of the chunk (sum for all docs)
+    if (chunkDocs == 1) {
+      skip = 0;
+      numFields = totalFields = vectorsStream.readVInt();
+    } else {
+      reader.reset(vectorsStream, chunkDocs);
+      int sum = 0;
+      for (int i = docBase; i < doc; ++i) {
+        sum += reader.next();
+      }
+      skip = sum;
+      numFields = (int) reader.next();
+      sum += numFields;
+      for (int i = doc + 1; i < docBase + chunkDocs; ++i) {
+        sum += reader.next();
+      }
+      totalFields = sum;
+    }
+
+    if (numFields == 0) {
+      // no vectors
+      return null;
+    }
+
+    // read field numbers that have term vectors
+    final int[] fieldNums;
+    {
+      final int token = vectorsStream.readByte() & 0xFF;
+      assert token != 0; // means no term vectors, cannot happen since we checked for numFields == 0
+      final int bitsPerFieldNum = token & 0x1F;
+      int totalDistinctFields = token >>> 5;
+      if (totalDistinctFields == 0x07) {
+        totalDistinctFields += vectorsStream.readVInt();
+      }
+      ++totalDistinctFields;
+      final PackedInts.ReaderIterator it = PackedInts.getReaderIteratorNoHeader(vectorsStream, PackedInts.Format.PACKED, packedIntsVersion, totalDistinctFields, bitsPerFieldNum, 1);
+      fieldNums = new int[totalDistinctFields];
+      for (int i = 0; i < totalDistinctFields; ++i) {
+        fieldNums[i] = (int) it.next();
+      }
+    }
+
+    // flags for encrypted field for all fields
+    final PackedInts.Reader encryptedFields;
+    {
+      encryptedFields = PackedInts.getReaderNoHeader(vectorsStream, PackedInts.Format.PACKED, packedIntsVersion, totalFields, 1);
+    }
+
+    // read field numbers and flags
+    final int[] fieldNumOffs = new int[numFields];
+    final PackedInts.Reader flags;
+    {
+      final int bitsPerOff = PackedInts.bitsRequired(fieldNums.length - 1);
+      final PackedInts.Reader allFieldNumOffs = PackedInts.getReaderNoHeader(vectorsStream, PackedInts.Format.PACKED, packedIntsVersion, totalFields, bitsPerOff);
+      switch (vectorsStream.readVInt()) {
+        case 0:
+          final PackedInts.Reader fieldFlags = PackedInts.getReaderNoHeader(vectorsStream, PackedInts.Format.PACKED, packedIntsVersion, fieldNums.length, FLAGS_BITS);
+          PackedInts.Mutable f = PackedInts.getMutable(totalFields, FLAGS_BITS, PackedInts.COMPACT);
+          for (int i = 0; i < totalFields; ++i) {
+            final int fieldNumOff = (int) allFieldNumOffs.get(i);
+            assert fieldNumOff >= 0 && fieldNumOff < fieldNums.length;
+            final int fgs = (int) fieldFlags.get(fieldNumOff);
+            f.set(i, fgs);
+          }
+          flags = f;
+          break;
+        case 1:
+          flags = PackedInts.getReaderNoHeader(vectorsStream, PackedInts.Format.PACKED, packedIntsVersion, totalFields, FLAGS_BITS);
+          break;
+        default:
+          throw new AssertionError();
+      }
+      for (int i = 0; i < numFields; ++i) {
+        fieldNumOffs[i] = (int) allFieldNumOffs.get(skip + i);
+      }
+    }
+
+    // number of terms per field for all fields
+    final PackedInts.Reader numTerms;
+    final int totalTerms;
+    {
+      final int bitsRequired = vectorsStream.readVInt();
+      numTerms = PackedInts.getReaderNoHeader(vectorsStream, PackedInts.Format.PACKED, packedIntsVersion, totalFields, bitsRequired);
+      int sum = 0;
+      for (int i = 0; i < totalFields; ++i) {
+        sum += numTerms.get(i);
+      }
+      totalTerms = sum;
+    }
+
+    // term lengths
+    int docOff = 0, docLen = 0, totalLen;
+    int encryptedDocOff = 0, encryptedDocLen = 0, encryptedTotalLen;
+    final int[] fieldLengths = new int[numFields];
+    final int[][] prefixLengths = new int[numFields][];
+    final int[][] suffixLengths = new int[numFields][];
+    {
+      reader.reset(vectorsStream, totalTerms);
+      // skip
+      int toSkip = 0;
+      for (int i = 0; i < skip; ++i) {
+        toSkip += numTerms.get(i);
+      }
+      reader.skip(toSkip);
+      // read prefix lengths
+      for (int i = 0; i < numFields; ++i) {
+        final int termCount = (int) numTerms.get(skip + i);
+        final int[] fieldPrefixLengths = new int[termCount];
+        prefixLengths[i] = fieldPrefixLengths;
+        for (int j = 0; j < termCount; ) {
+          final LongsRef next = reader.next(termCount - j);
+          for (int k = 0; k < next.length; ++k) {
+            fieldPrefixLengths[j++] = (int) next.longs[next.offset + k];
+          }
+        }
+      }
+      reader.skip(totalTerms - reader.ord());
+
+      reader.reset(vectorsStream, totalTerms);
+      // skip
+      toSkip = 0;
+      for (int i = 0; i < skip; ++i) {
+        boolean isEncrypted = encryptedFields.get(i) == 1 ? true : false;
+        for (int j = 0; j < numTerms.get(i); ++j) {
+          if (!isEncrypted) {
+            docOff += reader.next();
+          }
+          else {
+            encryptedDocOff += reader.next();
+          }
+        }
+      }
+
+      for (int i = 0; i < numFields; ++i) {
+        final int termCount = (int) numTerms.get(skip + i);
+        final int[] fieldSuffixLengths = new int[termCount];
+        suffixLengths[i] = fieldSuffixLengths;
+        for (int j = 0; j < termCount; ) {
+          final LongsRef next = reader.next(termCount - j);
+          for (int k = 0; k < next.length; ++k) {
+            fieldSuffixLengths[j++] = (int) next.longs[next.offset + k];
+          }
+        }
+        fieldLengths[i] = sum(suffixLengths[i]);
+        boolean isEncrypted = encryptedFields.get(skip + i) == 1 ? true : false;
+        if (!isEncrypted) {
+          docLen += fieldLengths[i];
+        }
+        else {
+          encryptedDocLen += fieldLengths[i];
+        }
+      }
+
+      totalLen = docOff + docLen;
+      encryptedTotalLen = encryptedDocOff + encryptedDocLen;
+      for (int i = skip + numFields; i < totalFields; ++i) {
+        boolean isEncrypted = encryptedFields.get(i) == 1 ? true : false;
+        for (int j = 0; j < numTerms.get(i); ++j) {
+          if (!isEncrypted) {
+            totalLen += reader.next();
+          }
+          else {
+            encryptedTotalLen += reader.next();
+          }
+        }
+      }
+    }
+
+    // term freqs
+    final int[] termFreqs = new int[totalTerms];
+    {
+      reader.reset(vectorsStream, totalTerms);
+      for (int i = 0; i < totalTerms; ) {
+        final LongsRef next = reader.next(totalTerms - i);
+        for (int k = 0; k < next.length; ++k) {
+          termFreqs[i++] = 1 + (int) next.longs[next.offset + k];
+        }
+      }
+    }
+
+    // total number of positions, offsets and payloads
+    int totalPositions = 0, totalOffsets = 0, totalPayloads = 0;
+    for (int i = 0, termIndex = 0; i < totalFields; ++i) {
+      final int f = (int) flags.get(i);
+      final int termCount = (int) numTerms.get(i);
+      for (int j = 0; j < termCount; ++j) {
+        final int freq = termFreqs[termIndex++];
+        if ((f & POSITIONS) != 0) {
+          totalPositions += freq;
+        }
+        if ((f & OFFSETS) != 0) {
+          totalOffsets += freq;
+        }
+        if ((f & PAYLOADS) != 0) {
+          totalPayloads += freq;
+        }
+      }
+      assert i != totalFields - 1 || termIndex == totalTerms : termIndex + " " + totalTerms;
+    }
+
+    final int[][] positionIndex = positionIndex(skip, numFields, numTerms, termFreqs);
+    final int[][] positions, startOffsets, lengths;
+    if (totalPositions > 0) {
+      positions = readPositions(skip, numFields, flags, numTerms, termFreqs, POSITIONS, totalPositions, positionIndex);
+    } else {
+      positions = new int[numFields][];
+    }
+
+    if (totalOffsets > 0) {
+      // average number of chars per term
+      final float[] charsPerTerm = new float[fieldNums.length];
+      for (int i = 0; i < charsPerTerm.length; ++i) {
+        charsPerTerm[i] = Float.intBitsToFloat(vectorsStream.readInt());
+      }
+      startOffsets = readPositions(skip, numFields, flags, numTerms, termFreqs, OFFSETS, totalOffsets, positionIndex);
+      lengths = readPositions(skip, numFields, flags, numTerms, termFreqs, OFFSETS, totalOffsets, positionIndex);
+
+      for (int i = 0; i < numFields; ++i) {
+        final int[] fStartOffsets = startOffsets[i];
+        final int[] fPositions = positions[i];
+        // patch offsets from positions
+        if (fStartOffsets != null && fPositions != null) {
+          final float fieldCharsPerTerm = charsPerTerm[fieldNumOffs[i]];
+          for (int j = 0; j < startOffsets[i].length; ++j) {
+            fStartOffsets[j] += (int) (fieldCharsPerTerm * fPositions[j]);
+          }
+        }
+        if (fStartOffsets != null) {
+          final int[] fPrefixLengths = prefixLengths[i];
+          final int[] fSuffixLengths = suffixLengths[i];
+          final int[] fLengths = lengths[i];
+          for (int j = 0, end = (int) numTerms.get(skip + i); j < end; ++j) {
+            // delta-decode start offsets and  patch lengths using term lengths
+            final int termLength = fPrefixLengths[j] + fSuffixLengths[j];
+            lengths[i][positionIndex[i][j]] += termLength;
+            for (int k = positionIndex[i][j] + 1; k < positionIndex[i][j + 1]; ++k) {
+              fStartOffsets[k] += fStartOffsets[k - 1];
+              fLengths[k] += termLength;
+            }
+          }
+        }
+      }
+    } else {
+      startOffsets = lengths = new int[numFields][];
+    }
+    if (totalPositions > 0) {
+      // delta-decode positions
+      for (int i = 0; i < numFields; ++i) {
+        final int[] fPositions = positions[i];
+        final int[] fpositionIndex = positionIndex[i];
+        if (fPositions != null) {
+          for (int j = 0, end = (int) numTerms.get(skip + i); j < end; ++j) {
+            // delta-decode start offsets
+            for (int k = fpositionIndex[j] + 1; k < fpositionIndex[j + 1]; ++k) {
+              fPositions[k] += fPositions[k - 1];
+            }
+          }
+        }
+      }
+    }
+
+    // payload lengths
+    final int[][] payloadIndex = new int[numFields][];
+    int totalPayloadLength = 0;
+    int payloadOff = 0;
+    int payloadLen = 0;
+    int encryptedPayloadOff = 0, encryptedPayloadLen = 0, encryptedTotalPayloadLength = 0;
+    if (totalPayloads > 0) {
+      reader.reset(vectorsStream, totalPayloads);
+      // skip
+      int termIndex = 0;
+      for (int i = 0; i < skip; ++i) {
+        final int f = (int) flags.get(i);
+        final int termCount = (int) numTerms.get(i);
+        final boolean isEncrypted = encryptedFields.get(i) == 1 ? true : false;
+        if ((f & PAYLOADS) != 0) {
+          for (int j = 0; j < termCount; ++j) {
+            final int freq = termFreqs[termIndex + j];
+            for (int k = 0; k < freq; ++k) {
+              final int l = (int) reader.next();
+              if (!isEncrypted) {
+                payloadOff += l;
+              }
+              else {
+                encryptedPayloadOff += l;
+              }
+            }
+          }
+        }
+        termIndex += termCount;
+      }
+      totalPayloadLength = payloadOff;
+      encryptedTotalPayloadLength = encryptedPayloadOff;
+      // read doc payload lengths
+      for (int i = 0; i < numFields; ++i) {
+        final int f = (int) flags.get(skip + i);
+        final int termCount = (int) numTerms.get(skip + i);
+        final boolean isEncrypted = encryptedFields.get(skip + i) == 1 ? true : false;
+        if ((f & PAYLOADS) != 0) {
+          final int totalFreq = positionIndex[i][termCount];
+          payloadIndex[i] = new int[totalFreq + 1];
+          int posIdx = 0;
+          payloadIndex[i][posIdx] = isEncrypted ? encryptedPayloadLen : payloadLen;
+          for (int j = 0; j < termCount; ++j) {
+            final int freq = termFreqs[termIndex + j];
+            for (int k = 0; k < freq; ++k) {
+              final int payloadLength = (int) reader.next();
+              if (!isEncrypted) {
+                payloadLen += payloadLength;
+              }
+              else {
+                encryptedPayloadLen += payloadLength;
+              }
+              payloadIndex[i][posIdx+1] = isEncrypted ? encryptedPayloadLen : payloadLen;
+              ++posIdx;
+            }
+          }
+          assert posIdx == totalFreq;
+        }
+        termIndex += termCount;
+      }
+      totalPayloadLength += payloadLen;
+      encryptedTotalPayloadLength += encryptedPayloadLen;
+      for (int i = skip + numFields; i < totalFields; ++i) {
+        final int f = (int) flags.get(i);
+        final int termCount = (int) numTerms.get(i);
+        final boolean isEncrypted = encryptedFields.get(i) == 1 ? true : false;
+        if ((f & PAYLOADS) != 0) {
+          for (int j = 0; j < termCount; ++j) {
+            final int freq = termFreqs[termIndex + j];
+            for (int k = 0; k < freq; ++k) {
+              if (!isEncrypted) {
+                totalPayloadLength += reader.next();
+              }
+              else {
+                encryptedTotalPayloadLength += reader.next();
+              }
+            }
+          }
+        }
+        termIndex += termCount;
+      }
+      assert termIndex == totalTerms : termIndex + " " + totalTerms;
+    }
+
+    // decompress data
+    final BytesRef encryptedSuffixBytes = new BytesRef();
+    if (encryptedTotalLen + encryptedTotalPayloadLength > 0) {
+      decipherDecompressor.decompress(vectorsStream, encryptedTotalLen + encryptedTotalPayloadLength,
+          encryptedDocOff + encryptedPayloadOff, encryptedDocLen + encryptedPayloadLen, encryptedSuffixBytes);
+      encryptedSuffixBytes.length = encryptedDocLen;
+    }
+    final BytesRef encryptedPayloadBytes = new BytesRef(encryptedSuffixBytes.bytes, encryptedSuffixBytes.offset + encryptedDocLen, encryptedPayloadLen);
+
+    final BytesRef suffixBytes = new BytesRef();
+    if (totalLen + totalPayloadLength > 0) {
+      decompressor.decompress(vectorsStream, totalLen + totalPayloadLength, docOff + payloadOff, docLen + payloadLen, suffixBytes);
+      suffixBytes.length = docLen;
+    }
+    final BytesRef payloadBytes = new BytesRef(suffixBytes.bytes, suffixBytes.offset + docLen, payloadLen);
+
+    final int[] fieldFlags = new int[numFields];
+    for (int i = 0; i < numFields; ++i) {
+      fieldFlags[i] = (int) flags.get(skip + i);
+    }
+
+    final int[] fieldNumTerms = new int[numFields];
+    for (int i = 0; i < numFields; ++i) {
+      fieldNumTerms[i] = (int) numTerms.get(skip + i);
+    }
+
+    final boolean[] encryptedFieldFlags = new boolean[numFields];
+    for (int i = 0; i < numFields; ++i) {
+      encryptedFieldFlags[i] = encryptedFields.get(skip + i) == 1 ? true : false;
+    }
+
+    final int[][] fieldTermFreqs = new int[numFields][];
+    {
+      int termIdx = 0;
+      for (int i = 0; i < skip; ++i) {
+        termIdx += numTerms.get(i);
+      }
+      for (int i = 0; i < numFields; ++i) {
+        final int termCount = (int) numTerms.get(skip + i);
+        fieldTermFreqs[i] = new int[termCount];
+        for (int j = 0; j < termCount; ++j) {
+          fieldTermFreqs[i][j] = termFreqs[termIdx++];
+        }
+      }
+    }
+
+    assert sum(fieldLengths) == docLen + encryptedDocLen : sum(fieldLengths) + " != " + docLen;
+
+    return new TVFields(fieldNums, fieldFlags, encryptedFieldFlags, fieldNumOffs, fieldNumTerms, fieldLengths,
+        prefixLengths, suffixLengths, fieldTermFreqs,
+        positionIndex, positions, startOffsets, lengths,
+        payloadBytes, encryptedPayloadBytes, payloadIndex,
+        suffixBytes, encryptedSuffixBytes);
+  }
+
+  // field -> term index -> position index
+  private int[][] positionIndex(int skip, int numFields, PackedInts.Reader numTerms, int[] termFreqs) {
+    final int[][] positionIndex = new int[numFields][];
+    int termIndex = 0;
+    for (int i = 0; i < skip; ++i) {
+      final int termCount = (int) numTerms.get(i);
+      termIndex += termCount;
+    }
+    for (int i = 0; i < numFields; ++i) {
+      final int termCount = (int) numTerms.get(skip + i);
+      positionIndex[i] = new int[termCount + 1];
+      for (int j = 0; j < termCount; ++j) {
+        final int freq = termFreqs[termIndex+j];
+        positionIndex[i][j + 1] = positionIndex[i][j] + freq;
+      }
+      termIndex += termCount;
+    }
+    return positionIndex;
+  }
+
+  private int[][] readPositions(int skip, int numFields, PackedInts.Reader flags, PackedInts.Reader numTerms, int[] termFreqs, int flag, final int totalPositions, int[][] positionIndex) throws IOException {
+    final int[][] positions = new int[numFields][];
+    reader.reset(vectorsStream, totalPositions);
+    // skip
+    int toSkip = 0;
+    int termIndex = 0;
+    for (int i = 0; i < skip; ++i) {
+      final int f = (int) flags.get(i);
+      final int termCount = (int) numTerms.get(i);
+      if ((f & flag) != 0) {
+        for (int j = 0; j < termCount; ++j) {
+          final int freq = termFreqs[termIndex+j];
+          toSkip += freq;
+        }
+      }
+      termIndex += termCount;
+    }
+    reader.skip(toSkip);
+    // read doc positions
+    for (int i = 0; i < numFields; ++i) {
+      final int f = (int) flags.get(skip + i);
+      final int termCount = (int) numTerms.get(skip + i);
+      if ((f & flag) != 0) {
+        final int totalFreq = positionIndex[i][termCount];
+        final int[] fieldPositions = new int[totalFreq];
+        positions[i] = fieldPositions;
+        for (int j = 0; j < totalFreq; ) {
+          final LongsRef nextPositions = reader.next(totalFreq - j);
+          for (int k = 0; k < nextPositions.length; ++k) {
+            fieldPositions[j++] = (int) nextPositions.longs[nextPositions.offset + k];
+          }
+        }
+      }
+      termIndex += termCount;
+    }
+    reader.skip(totalPositions - reader.ord());
+    return positions;
+  }
+
+  private class TVFields extends Fields {
+
+    private final int[] fieldNums, fieldFlags, fieldNumOffs, numTerms, fieldLengths;
+    private final int[][] prefixLengths, suffixLengths, termFreqs, positionIndex, positions, startOffsets, lengths, payloadIndex;
+    private final BytesRef suffixBytes, payloadBytes, encryptedSuffixBytes, encryptedPayloadBytes;
+    private final boolean[] encryptedFieldFlags;
+
+    public TVFields(int[] fieldNums, int[] fieldFlags, boolean[] encryptedFieldFlags, int[] fieldNumOffs, int[] numTerms, int[] fieldLengths,
+        int[][] prefixLengths, int[][] suffixLengths, int[][] termFreqs,
+        int[][] positionIndex, int[][] positions, int[][] startOffsets, int[][] lengths,
+        BytesRef payloadBytes, BytesRef encryptedPayloadBytes, int[][] payloadIndex,
+        BytesRef suffixBytes, BytesRef encryptedSuffixBytes) {
+      this.fieldNums = fieldNums;
+      this.fieldFlags = fieldFlags;
+      this.encryptedFieldFlags = encryptedFieldFlags;
+      this.fieldNumOffs = fieldNumOffs;
+      this.numTerms = numTerms;
+      this.fieldLengths = fieldLengths;
+      this.prefixLengths = prefixLengths;
+      this.suffixLengths = suffixLengths;
+      this.termFreqs = termFreqs;
+      this.positionIndex = positionIndex;
+      this.positions = positions;
+      this.startOffsets = startOffsets;
+      this.lengths = lengths;
+      this.payloadBytes = payloadBytes;
+      this.payloadIndex = payloadIndex;
+      this.suffixBytes = suffixBytes;
+      this.encryptedSuffixBytes = encryptedSuffixBytes;
+      this.encryptedPayloadBytes = encryptedPayloadBytes;
+    }
+
+    @Override
+    public Iterator<String> iterator() {
+      return new Iterator<String>() {
+        int i = 0;
+        @Override
+        public boolean hasNext() {
+          return i < fieldNumOffs.length;
+        }
+        @Override
+        public String next() {
+          if (!hasNext()) {
+            throw new NoSuchElementException();
+          }
+          final int fieldNum = fieldNums[fieldNumOffs[i++]];
+          return fieldInfos.fieldInfo(fieldNum).name;
+        }
+        @Override
+        public void remove() {
+          throw new UnsupportedOperationException();
+        }
+      };
+    }
+
+    @Override
+    public Terms terms(String field) throws IOException {
+      final FieldInfo fieldInfo = fieldInfos.fieldInfo(field);
+      if (fieldInfo == null) {
+        return null;
+      }
+      int idx = -1;
+      for (int i = 0; i < fieldNumOffs.length; ++i) {
+        if (fieldNums[fieldNumOffs[i]] == fieldInfo.number) {
+          idx = i;
+          break;
+        }
+      }
+
+      if (idx == -1 || numTerms[idx] == 0) {
+        // no term
+        return null;
+      }
+
+      boolean isEncrypted = encryptedFieldFlags[idx];
+
+      int fieldOff = 0, fieldLen = -1;
+      for (int i = 0; i < fieldNumOffs.length; ++i) {
+        if (encryptedFieldFlags[i] == isEncrypted) { // only process fields of the same kind (encrypted vs non-ecnrypted)
+          if (i < idx) {
+            fieldOff += fieldLengths[i];
+          } else {
+            fieldLen = fieldLengths[i];
+            break;
+          }
+        }
+      }
+
+      BytesRef suffixBytes = isEncrypted ? this.encryptedSuffixBytes : this.suffixBytes;
+      BytesRef payloadBytes = isEncrypted ? this.encryptedPayloadBytes : this.payloadBytes;
+
+      assert fieldLen >= 0;
+      return new TVTerms(numTerms[idx], fieldFlags[idx],
+          prefixLengths[idx], suffixLengths[idx], termFreqs[idx],
+          positionIndex[idx], positions[idx], startOffsets[idx], lengths[idx],
+          payloadIndex[idx], payloadBytes,
+          new BytesRef(suffixBytes.bytes, suffixBytes.offset + fieldOff, fieldLen));
+    }
+
+    @Override
+    public int size() {
+      return fieldNumOffs.length;
+    }
+
+  }
+
+  private class TVTerms extends Terms {
+
+    private final int numTerms, flags;
+    private final int[] prefixLengths, suffixLengths, termFreqs, positionIndex, positions, startOffsets, lengths, payloadIndex;
+    private final BytesRef termBytes, payloadBytes;
+
+    TVTerms(int numTerms, int flags, int[] prefixLengths, int[] suffixLengths, int[] termFreqs,
+        int[] positionIndex, int[] positions, int[] startOffsets, int[] lengths,
+        int[] payloadIndex, BytesRef payloadBytes,
+        BytesRef termBytes) {
+      this.numTerms = numTerms;
+      this.flags = flags;
+      this.prefixLengths = prefixLengths;
+      this.suffixLengths = suffixLengths;
+      this.termFreqs = termFreqs;
+      this.positionIndex = positionIndex;
+      this.positions = positions;
+      this.startOffsets = startOffsets;
+      this.lengths = lengths;
+      this.payloadIndex = payloadIndex;
+      this.payloadBytes = payloadBytes;
+      this.termBytes = termBytes;
+    }
+
+    @Override
+    public TermsEnum iterator() throws IOException {
+      TVTermsEnum termsEnum = new TVTermsEnum();
+      termsEnum.reset(numTerms, flags, prefixLengths, suffixLengths, termFreqs, positionIndex, positions, startOffsets, lengths,
+          payloadIndex, payloadBytes,
+          new ByteArrayDataInput(termBytes.bytes, termBytes.offset, termBytes.length));
+      return termsEnum;
+    }
+
+    @Override
+    public long size() throws IOException {
+      return numTerms;
+    }
+
+    @Override
+    public long getSumTotalTermFreq() throws IOException {
+      return -1L;
+    }
+
+    @Override
+    public long getSumDocFreq() throws IOException {
+      return numTerms;
+    }
+
+    @Override
+    public int getDocCount() throws IOException {
+      return 1;
+    }
+
+    @Override
+    public boolean hasFreqs() {
+      return true;
+    }
+
+    @Override
+    public boolean hasOffsets() {
+      return (flags & OFFSETS) != 0;
+    }
+
+    @Override
+    public boolean hasPositions() {
+      return (flags & POSITIONS) != 0;
+    }
+
+    @Override
+    public boolean hasPayloads() {
+      return (flags & PAYLOADS) != 0;
+    }
+
+  }
+
+  private static class TVTermsEnum extends TermsEnum {
+
+    private int numTerms, startPos, ord;
+    private int[] prefixLengths, suffixLengths, termFreqs, positionIndex, positions, startOffsets, lengths, payloadIndex;
+    private ByteArrayDataInput in;
+    private BytesRef payloads;
+    private final BytesRef term;
+
+    private TVTermsEnum() {
+      term = new BytesRef(16);
+    }
+
+    void reset(int numTerms, int flags, int[] prefixLengths, int[] suffixLengths, int[] termFreqs, int[] positionIndex, int[] positions, int[] startOffsets, int[] lengths,
+        int[] payloadIndex, BytesRef payloads, ByteArrayDataInput in) {
+      this.numTerms = numTerms;
+      this.prefixLengths = prefixLengths;
+      this.suffixLengths = suffixLengths;
+      this.termFreqs = termFreqs;
+      this.positionIndex = positionIndex;
+      this.positions = positions;
+      this.startOffsets = startOffsets;
+      this.lengths = lengths;
+      this.payloadIndex = payloadIndex;
+      this.payloads = payloads;
+      this.in = in;
+      startPos = in.getPosition();
+      reset();
+    }
+
+    void reset() {
+      term.length = 0;
+      in.setPosition(startPos);
+      ord = -1;
+    }
+
+    @Override
+    public BytesRef next() throws IOException {
+      if (ord == numTerms - 1) {
+        return null;
+      } else {
+        assert ord < numTerms;
+        ++ord;
+      }
+
+      // read term
+      term.offset = 0;
+      term.length = prefixLengths[ord] + suffixLengths[ord];
+      if (term.length > term.bytes.length) {
+        term.bytes = ArrayUtil.grow(term.bytes, term.length);
+      }
+      in.readBytes(term.bytes, prefixLengths[ord], suffixLengths[ord]);
+
+      return term;
+    }
+
+    @Override
+    public SeekStatus seekCeil(BytesRef text)
+        throws IOException {
+      if (ord < numTerms && ord >= 0) {
+        final int cmp = term().compareTo(text);
+        if (cmp == 0) {
+          return SeekStatus.FOUND;
+        } else if (cmp > 0) {
+          reset();
+        }
+      }
+      // linear scan
+      while (true) {
+        final BytesRef term = next();
+        if (term == null) {
+          return SeekStatus.END;
+        }
+        final int cmp = term.compareTo(text);
+        if (cmp > 0) {
+          return SeekStatus.NOT_FOUND;
+        } else if (cmp == 0) {
+          return SeekStatus.FOUND;
+        }
+      }
+    }
+
+    @Override
+    public void seekExact(long ord) throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public BytesRef term() throws IOException {
+      return term;
+    }
+
+    @Override
+    public long ord() throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int docFreq() throws IOException {
+      return 1;
+    }
+
+    @Override
+    public long totalTermFreq() throws IOException {
+      return termFreqs[ord];
+    }
+
+    @Override
+    public final PostingsEnum postings(PostingsEnum reuse, int flags) throws IOException {
+      final TVPostingsEnum docsEnum;
+      if (reuse != null && reuse instanceof TVPostingsEnum) {
+        docsEnum = (TVPostingsEnum) reuse;
+      } else {
+        docsEnum = new TVPostingsEnum();
+      }
+
+      docsEnum.reset(termFreqs[ord], positionIndex[ord], positions, startOffsets, lengths, payloads, payloadIndex);
+      return docsEnum;
+    }
+
+  }
+
+  private static class TVPostingsEnum extends PostingsEnum {
+
+    private int doc = -1;
+    private int termFreq;
+    private int positionIndex;
+    private int[] positions;
+    private int[] startOffsets;
+    private int[] lengths;
+    private final BytesRef payload;
+    private int[] payloadIndex;
+    private int basePayloadOffset;
+    private int i;
+
+    TVPostingsEnum() {
+      payload = new BytesRef();
+    }
+
+    public void reset(int freq, int positionIndex, int[] positions,
+        int[] startOffsets, int[] lengths, BytesRef payloads,
+        int[] payloadIndex) {
+      this.termFreq = freq;
+      this.positionIndex = positionIndex;
+      this.positions = positions;
+      this.startOffsets = startOffsets;
+      this.lengths = lengths;
+      this.basePayloadOffset = payloads.offset;
+      this.payload.bytes = payloads.bytes;
+      payload.offset = payload.length = 0;
+      this.payloadIndex = payloadIndex;
+
+      doc = i = -1;
+    }
+
+    private void checkDoc() {
+      if (doc == NO_MORE_DOCS) {
+        throw new IllegalStateException("DocsEnum exhausted");
+      } else if (doc == -1) {
+        throw new IllegalStateException("DocsEnum not started");
+      }
+    }
+
+    private void checkPosition() {
+      checkDoc();
+      if (i < 0) {
+        throw new IllegalStateException("Position enum not started");
+      } else if (i >= termFreq) {
+        throw new IllegalStateException("Read past last position");
+      }
+    }
+
+    @Override
+    public int nextPosition() throws IOException {
+      if (doc != 0) {
+        throw new IllegalStateException();
+      } else if (i >= termFreq - 1) {
+        throw new IllegalStateException("Read past last position");
+      }
+
+      ++i;
+
+      if (payloadIndex != null) {
+        payload.offset = basePayloadOffset + payloadIndex[positionIndex + i];
+        payload.length = payloadIndex[positionIndex + i + 1] - payloadIndex[positionIndex + i];
+      }
+
+      if (positions == null) {
+        return -1;
+      } else {
+        return positions[positionIndex + i];
+      }
+    }
+
+    @Override
+    public int startOffset() throws IOException {
+      checkPosition();
+      if (startOffsets == null) {
+        return -1;
+      } else {
+        return startOffsets[positionIndex + i];
+      }
+    }
+
+    @Override
+    public int endOffset() throws IOException {
+      checkPosition();
+      if (startOffsets == null) {
+        return -1;
+      } else {
+        return startOffsets[positionIndex + i] + lengths[positionIndex + i];
+      }
+    }
+
+    @Override
+    public BytesRef getPayload() throws IOException {
+      checkPosition();
+      if (payloadIndex == null || payload.length == 0) {
+        return null;
+      } else {
+        return payload;
+      }
+    }
+
+    @Override
+    public int freq() throws IOException {
+      checkDoc();
+      return termFreq;
+    }
+
+    @Override
+    public int docID() {
+      return doc;
+    }
+
+    @Override
+    public int nextDoc() throws IOException {
+      if (doc == -1) {
+        return (doc = 0);
+      } else {
+        return (doc = NO_MORE_DOCS);
+      }
+    }
+
+    @Override
+    public int advance(int target) throws IOException {
+      return slowAdvance(target);
+    }
+
+    @Override
+    public long cost() {
+      return 1;
+    }
+  }
+
+  private static int sum(int[] arr) {
+    int sum = 0;
+    for (int el : arr) {
+      sum += el;
+    }
+    return sum;
+  }
+
+  @Override
+  public long ramBytesUsed() {
+    return indexReader.ramBytesUsed();
+  }
+  
+  @Override
+  public Collection<Accountable> getChildResources() {
+    return Collections.singleton(Accountables.namedAccountable("term vector index", indexReader));
+  }
+  
+  @Override
+  public void checkIntegrity() throws IOException {
+    CodecUtil.checksumEntireFile(vectorsStream);
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName() + "(mode=" + compressionMode + ",chunksize=" + chunkSize + ")";
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingTermVectorsWriter.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingTermVectorsWriter.java
new file mode 100644
index 0000000..8c97c15
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedCompressingTermVectorsWriter.java
@@ -0,0 +1,925 @@
+package org.apache.lucene.codecs.encrypted.compressing;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.CodecUtil;
+import org.apache.lucene.codecs.TermVectorsReader;
+import org.apache.lucene.codecs.TermVectorsWriter;
+import org.apache.lucene.codecs.compressing.CompressionMode;
+import org.apache.lucene.codecs.compressing.Compressor;
+import org.apache.lucene.codecs.compressing.GrowableByteArrayDataOutput;
+import org.apache.lucene.codecs.encrypted.CipherFactory;
+import org.apache.lucene.index.CorruptIndexException;
+import org.apache.lucene.index.FieldInfo;
+import org.apache.lucene.index.FieldInfos;
+import org.apache.lucene.index.Fields;
+import org.apache.lucene.index.IndexFileNames;
+import org.apache.lucene.index.MergeState;
+import org.apache.lucene.index.SegmentInfo;
+import org.apache.lucene.store.DataInput;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.IOContext;
+import org.apache.lucene.store.IndexInput;
+import org.apache.lucene.store.IndexOutput;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.Bits;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.IOUtils;
+import org.apache.lucene.util.StringHelper;
+import org.apache.lucene.util.packed.BlockPackedWriter;
+import org.apache.lucene.util.packed.PackedInts;
+
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * {@link TermVectorsWriter} for {@link EncryptedCompressingTermVectorsFormat}.
+ * @lucene.experimental
+ */
+public final class EncryptedCompressingTermVectorsWriter extends TermVectorsWriter {
+
+  // hard limit on the maximum number of documents per chunk
+  static final int MAX_DOCUMENTS_PER_CHUNK = 128;
+
+  static final String VECTORS_EXTENSION = "tvd";
+  static final String VECTORS_INDEX_EXTENSION = "tvx";
+
+  static final String CODEC_SFX_IDX = "Index";
+  static final String CODEC_SFX_DAT = "Data";
+
+  static final int VERSION_START = 0;
+  static final int VERSION_CHUNK_STATS = 1;
+  static final int VERSION_CURRENT = VERSION_CHUNK_STATS;
+
+  static final int PACKED_BLOCK_SIZE = 64;
+
+  static final int POSITIONS = 0x01;
+  static final int   OFFSETS = 0x02;
+  static final int  PAYLOADS = 0x04;
+  static final int FLAGS_BITS = PackedInts.bitsRequired(POSITIONS | OFFSETS | PAYLOADS);
+
+  private final String segment;
+  private CompressingStoredFieldsIndexWriter indexWriter;
+  private IndexOutput vectorsStream;
+
+  private final CompressionMode compressionMode;
+  private final Compressor compressor;
+  private final Compressor encipherCompressor;
+  private final int chunkSize;
+
+  private long numChunks; // number of compressed blocks written
+  private long numDirtyChunks; // number of incomplete compressed blocks written
+
+  private long encipherVersion;
+  private EncryptedCompressingTermVectorsFormat parentFormat;
+
+  /** a pending doc */
+  private class DocData {
+    final int numFields;
+    final Deque<FieldData> fields;
+    final int posStart, offStart, payStart;
+    DocData(int numFields, int posStart, int offStart, int payStart) {
+      this.numFields = numFields;
+      this.fields = new ArrayDeque<>(numFields);
+      this.posStart = posStart;
+      this.offStart = offStart;
+      this.payStart = payStart;
+    }
+    FieldData addField(int fieldNum, boolean isEncrypted, int numTerms, boolean positions, boolean offsets, boolean payloads) {
+      final FieldData field;
+      if (fields.isEmpty()) {
+        field = new FieldData(fieldNum, isEncrypted, numTerms, positions, offsets, payloads, posStart, offStart, payStart);
+      } else {
+        final FieldData last = fields.getLast();
+        final int posStart = last.posStart + (last.hasPositions ? last.totalPositions : 0);
+        final int offStart = last.offStart + (last.hasOffsets ? last.totalPositions : 0);
+        final int payStart = last.payStart + (last.hasPayloads ? last.totalPositions : 0);
+        field = new FieldData(fieldNum, isEncrypted, numTerms, positions, offsets, payloads, posStart, offStart, payStart);
+      }
+      fields.add(field);
+      return field;
+    }
+  }
+
+  private DocData addDocData(int numVectorFields) {
+    FieldData last = null;
+    for (Iterator<DocData> it = pendingDocs.descendingIterator(); it.hasNext(); ) {
+      final DocData doc = it.next();
+      if (!doc.fields.isEmpty()) {
+        last = doc.fields.getLast();
+        break;
+      }
+    }
+    final DocData doc;
+    if (last == null) {
+      doc = new DocData(numVectorFields, 0, 0, 0);
+    } else {
+      final int posStart = last.posStart + (last.hasPositions ? last.totalPositions : 0);
+      final int offStart = last.offStart + (last.hasOffsets ? last.totalPositions : 0);
+      final int payStart = last.payStart + (last.hasPayloads ? last.totalPositions : 0);
+      doc = new DocData(numVectorFields, posStart, offStart, payStart);
+    }
+    pendingDocs.add(doc);
+    return doc;
+  }
+
+  /** a pending field */
+  private class FieldData {
+    final boolean hasPositions, hasOffsets, hasPayloads;
+    final int fieldNum, flags, numTerms;
+    final int[] freqs, prefixLengths, suffixLengths;
+    final int posStart, offStart, payStart;
+    int totalPositions;
+    int ord;
+    boolean isEncrypted;
+    FieldData(int fieldNum, boolean isEncrypted, int numTerms, boolean positions, boolean offsets, boolean payloads,
+        int posStart, int offStart, int payStart) {
+      this.fieldNum = fieldNum;
+      this.isEncrypted = isEncrypted;
+      this.numTerms = numTerms;
+      this.hasPositions = positions;
+      this.hasOffsets = offsets;
+      this.hasPayloads = payloads;
+      this.flags = (positions ? POSITIONS : 0) | (offsets ? OFFSETS : 0) | (payloads ? PAYLOADS : 0);
+      this.freqs = new int[numTerms];
+      this.prefixLengths = new int[numTerms];
+      this.suffixLengths = new int[numTerms];
+      this.posStart = posStart;
+      this.offStart = offStart;
+      this.payStart = payStart;
+      totalPositions = 0;
+      ord = 0;
+    }
+    void addTerm(int freq, int prefixLength, int suffixLength) {
+      freqs[ord] = freq;
+      prefixLengths[ord] = prefixLength;
+      suffixLengths[ord] = suffixLength;
+      ++ord;
+    }
+    void addPosition(int position, int startOffset, int length, int payloadLength) {
+      if (hasPositions) {
+        if (posStart + totalPositions == positionsBuf.length) {
+          positionsBuf = ArrayUtil.grow(positionsBuf);
+        }
+        positionsBuf[posStart + totalPositions] = position;
+      }
+      if (hasOffsets) {
+        if (offStart + totalPositions == startOffsetsBuf.length) {
+          final int newLength = ArrayUtil.oversize(offStart + totalPositions, 4);
+          startOffsetsBuf = Arrays.copyOf(startOffsetsBuf, newLength);
+          lengthsBuf = Arrays.copyOf(lengthsBuf, newLength);
+        }
+        startOffsetsBuf[offStart + totalPositions] = startOffset;
+        lengthsBuf[offStart + totalPositions] = length;
+      }
+      if (hasPayloads) {
+        if (payStart + totalPositions == payloadLengthsBuf.length) {
+          payloadLengthsBuf = ArrayUtil.grow(payloadLengthsBuf);
+        }
+        payloadLengthsBuf[payStart + totalPositions] = payloadLength;
+      }
+      ++totalPositions;
+    }
+  }
+
+  private int numDocs; // total number of docs seen
+  private final Deque<DocData> pendingDocs; // pending docs
+  private DocData curDoc; // current document
+  private FieldData curField; // current field
+  private final BytesRef lastTerm;
+  private int[] positionsBuf, startOffsetsBuf, lengthsBuf, payloadLengthsBuf;
+  private final GrowableByteArrayDataOutput termSuffixes; // buffered term suffixes
+  private final GrowableByteArrayDataOutput encryptedTermSuffixes; // buffered encrypted term suffixes
+  private final GrowableByteArrayDataOutput payloadBytes; // buffered term payloads
+  private final GrowableByteArrayDataOutput encryptedPayloadBytes; // buffered encrypted term payloads
+  private final BlockPackedWriter writer;
+
+  /** Sole constructor. */
+  public EncryptedCompressingTermVectorsWriter(EncryptedCompressingTermVectorsFormat parentFormat,
+                                               Directory directory, SegmentInfo si, String segmentSuffix, IOContext context,
+                                               String formatName, CompressionMode compressionMode, int chunkSize, int blockSize) throws IOException {
+    assert directory != null;
+    this.parentFormat = parentFormat;
+    this.encipherVersion = parentFormat.getCipherFactory().getEncipherVersion();
+    this.segment = si.name;
+    this.compressionMode = compressionMode;
+    this.compressor = compressionMode.newCompressor();
+    this.encipherCompressor = new EncipherCompressor(parentFormat.getCipherFactory().newEncipherInstance(), compressor);
+    this.chunkSize = chunkSize;
+
+    numDocs = 0;
+    pendingDocs = new ArrayDeque<>();
+    termSuffixes = new GrowableByteArrayDataOutput(ArrayUtil.oversize(chunkSize, 1));
+    encryptedTermSuffixes = new GrowableByteArrayDataOutput(ArrayUtil.oversize(chunkSize, 1));
+    payloadBytes = new GrowableByteArrayDataOutput(ArrayUtil.oversize(1, 1));
+    encryptedPayloadBytes = new GrowableByteArrayDataOutput(ArrayUtil.oversize(1, 1));
+    lastTerm = new BytesRef(ArrayUtil.oversize(30, 1));
+
+    boolean success = false;
+    IndexOutput indexStream = directory.createOutput(IndexFileNames.segmentFileName(segment, segmentSuffix, VECTORS_INDEX_EXTENSION),
+                                                                     context);
+    try {
+      vectorsStream = directory.createOutput(IndexFileNames.segmentFileName(segment, segmentSuffix, VECTORS_EXTENSION),
+                                                     context);
+
+      final String codecNameIdx = formatName + CODEC_SFX_IDX;
+      final String codecNameDat = formatName + CODEC_SFX_DAT;
+      CodecUtil.writeIndexHeader(indexStream, codecNameIdx, VERSION_CURRENT, si.getId(), segmentSuffix);
+      CodecUtil.writeIndexHeader(vectorsStream, codecNameDat, VERSION_CURRENT, si.getId(), segmentSuffix);
+      assert CodecUtil.indexHeaderLength(codecNameDat, segmentSuffix) == vectorsStream.getFilePointer();
+      assert CodecUtil.indexHeaderLength(codecNameIdx, segmentSuffix) == indexStream.getFilePointer();
+
+      indexWriter = new CompressingStoredFieldsIndexWriter(indexStream, blockSize);
+      indexStream = null;
+
+      vectorsStream.writeVInt(PackedInts.VERSION_CURRENT);
+      vectorsStream.writeVInt(chunkSize);
+      writer = new BlockPackedWriter(vectorsStream, PACKED_BLOCK_SIZE);
+
+      positionsBuf = new int[1024];
+      startOffsetsBuf = new int[1024];
+      lengthsBuf = new int[1024];
+      payloadLengthsBuf = new int[1024];
+
+      success = true;
+    } finally {
+      if (!success) {
+        IOUtils.closeWhileHandlingException(vectorsStream, indexStream, indexWriter);
+      }
+    }
+  }
+
+  @Override
+  public void close() throws IOException {
+    try {
+      IOUtils.close(vectorsStream, indexWriter);
+    } finally {
+      vectorsStream = null;
+      indexWriter = null;
+    }
+  }
+
+  @Override
+  public void startDocument(int numVectorFields) throws IOException {
+    curDoc = addDocData(numVectorFields);
+  }
+
+  @Override
+  public void finishDocument() throws IOException {
+    // append the payload bytes of the doc after its terms
+    termSuffixes.writeBytes(payloadBytes.bytes, payloadBytes.length);
+    encryptedTermSuffixes.writeBytes(encryptedPayloadBytes.bytes, encryptedPayloadBytes.length);
+    payloadBytes.length = 0;
+    encryptedPayloadBytes.length = 0;
+    ++numDocs;
+    if (triggerFlush()) {
+      flush();
+    }
+    curDoc = null;
+  }
+
+  @Override
+  public void startField(FieldInfo info, int numTerms, boolean positions,
+      boolean offsets, boolean payloads) throws IOException {
+    boolean isEncrypted = parentFormat.isFieldEncrypted(info.name) ? true : false;
+    curField = curDoc.addField(info.number, isEncrypted, numTerms, positions, offsets, payloads);
+    lastTerm.length = 0;
+  }
+
+  @Override
+  public void finishField() throws IOException {
+    curField = null;
+  }
+
+  @Override
+  public void startTerm(BytesRef term, int freq) throws IOException {
+    assert freq >= 1;
+    final int prefix = StringHelper.bytesDifference(lastTerm, term);
+    curField.addTerm(freq, prefix, term.length - prefix);
+    if (!curField.isEncrypted) {
+      termSuffixes.writeBytes(term.bytes, term.offset + prefix, term.length - prefix);
+    }
+    else {
+      encryptedTermSuffixes.writeBytes(term.bytes, term.offset + prefix, term.length - prefix);
+    }
+    // copy last term
+    if (lastTerm.bytes.length < term.length) {
+      lastTerm.bytes = new byte[ArrayUtil.oversize(term.length, 1)];
+    }
+    lastTerm.offset = 0;
+    lastTerm.length = term.length;
+    System.arraycopy(term.bytes, term.offset, lastTerm.bytes, 0, term.length);
+  }
+
+  @Override
+  public void addPosition(int position, int startOffset, int endOffset,
+      BytesRef payload) throws IOException {
+    assert curField.flags != 0;
+    curField.addPosition(position, startOffset, endOffset - startOffset, payload == null ? 0 : payload.length);
+    if (curField.hasPayloads && payload != null) {
+      if (!curField.isEncrypted) {
+        payloadBytes.writeBytes(payload.bytes, payload.offset, payload.length);
+      }
+      else {
+        encryptedPayloadBytes.writeBytes(payload.bytes, payload.offset, payload.length);
+      }
+    }
+  }
+
+  private boolean triggerFlush() {
+    return termSuffixes.length >= chunkSize || encryptedTermSuffixes.length >= chunkSize
+        || pendingDocs.size() >= MAX_DOCUMENTS_PER_CHUNK;
+  }
+
+  private void flush() throws IOException {
+    final int chunkDocs = pendingDocs.size();
+    assert chunkDocs > 0 : chunkDocs;
+
+    // write the index file
+    indexWriter.writeIndex(chunkDocs, vectorsStream.getFilePointer());
+
+    final int docBase = numDocs - chunkDocs;
+    vectorsStream.writeVInt(docBase);
+    vectorsStream.writeVInt(chunkDocs);
+
+    // total number of fields of the chunk
+    final int totalFields = flushNumFields(chunkDocs);
+
+    if (totalFields > 0) {
+      // unique field numbers (sorted)
+      final int[] fieldNums = flushFieldNums();
+      // encrypted fields index
+      flushEncryptedFieldsIndex(totalFields);
+      // offsets in the array of unique field numbers
+      flushFields(totalFields, fieldNums);
+      // flags (does the field have positions, offsets, payloads?)
+      flushFlags(totalFields, fieldNums);
+      // number of terms of each field
+      flushNumTerms(totalFields);
+      // prefix and suffix lengths for each field
+      flushTermLengths();
+      // term freqs - 1 (because termFreq is always >=1) for each term
+      flushTermFreqs();
+      // positions for all terms, when enabled
+      flushPositions();
+      // offsets for all terms, when enabled
+      flushOffsets(fieldNums);
+      // payload lengths for all terms, when enabled
+      flushPayloadLengths();
+
+      // encrypt and compress terms and payloads and write them to the output
+      // we first write the encrypted data block, as it will always be fully read, unlike the non-encrypted one,
+      // and rely on that in the reader to start reading the non-encrypted block at the correct fp
+      if (encryptedTermSuffixes.length > 0) {
+        encipherCompressor.compress(encryptedTermSuffixes.bytes, 0, encryptedTermSuffixes.length, vectorsStream);
+      }
+
+      // compress terms and payloads and write them to the output
+      if (termSuffixes.length > 0) {
+        compressor.compress(termSuffixes.bytes, 0, termSuffixes.length, vectorsStream);
+      }
+    }
+
+    // reset
+    pendingDocs.clear();
+    curDoc = null;
+    curField = null;
+    termSuffixes.length = 0;
+    encryptedTermSuffixes.length = 0;
+    numChunks++;
+  }
+
+  private int flushNumFields(int chunkDocs) throws IOException {
+    if (chunkDocs == 1) {
+      final int numFields = pendingDocs.getFirst().numFields;
+      vectorsStream.writeVInt(numFields);
+      return numFields;
+    } else {
+      writer.reset(vectorsStream);
+      int totalFields = 0;
+      for (DocData dd : pendingDocs) {
+        writer.add(dd.numFields);
+        totalFields += dd.numFields;
+      }
+      writer.finish();
+      return totalFields;
+    }
+  }
+
+  /** Returns a sorted array containing unique field numbers */
+  private int[] flushFieldNums() throws IOException {
+    SortedSet<Integer> fieldNums = new TreeSet<>();
+    for (DocData dd : pendingDocs) {
+      for (FieldData fd : dd.fields) {
+        fieldNums.add(fd.fieldNum);
+      }
+    }
+
+    final int numDistinctFields = fieldNums.size();
+    assert numDistinctFields > 0;
+    final int bitsRequired = PackedInts.bitsRequired(fieldNums.last());
+    final int token = (Math.min(numDistinctFields - 1, 0x07) << 5) | bitsRequired;
+    vectorsStream.writeByte((byte) token);
+    if (numDistinctFields - 1 >= 0x07) {
+      vectorsStream.writeVInt(numDistinctFields - 1 - 0x07);
+    }
+    final PackedInts.Writer writer = PackedInts.getWriterNoHeader(vectorsStream, PackedInts.Format.PACKED, fieldNums.size(), bitsRequired, 1);
+    for (Integer fieldNum : fieldNums) {
+      writer.add(fieldNum);
+    }
+    writer.finish();
+
+    int[] fns = new int[fieldNums.size()];
+    int i = 0;
+    for (Integer key : fieldNums) {
+      fns[i++] = key;
+    }
+    return fns;
+  }
+
+  private void flushEncryptedFieldsIndex(int totalFields) throws IOException {
+    final PackedInts.Writer writer = PackedInts.getWriterNoHeader(vectorsStream, PackedInts.Format.PACKED, totalFields, 1, 1);
+    for (DocData dd : pendingDocs) {
+      for (FieldData fd : dd.fields) {
+        writer.add(fd.isEncrypted ? 1 : 0);
+      }
+    }
+    writer.finish();
+  }
+
+  private void flushFields(int totalFields, int[] fieldNums) throws IOException {
+    final PackedInts.Writer writer = PackedInts.getWriterNoHeader(vectorsStream, PackedInts.Format.PACKED, totalFields, PackedInts.bitsRequired(fieldNums.length - 1), 1);
+    for (DocData dd : pendingDocs) {
+      for (FieldData fd : dd.fields) {
+        final int fieldNumIndex = Arrays.binarySearch(fieldNums, fd.fieldNum);
+        assert fieldNumIndex >= 0;
+        writer.add(fieldNumIndex);
+      }
+    }
+    writer.finish();
+  }
+
+  private void flushFlags(int totalFields, int[] fieldNums) throws IOException {
+    // check if fields always have the same flags
+    boolean nonChangingFlags = true;
+    int[] fieldFlags = new int[fieldNums.length];
+    Arrays.fill(fieldFlags, -1);
+    outer:
+    for (DocData dd : pendingDocs) {
+      for (FieldData fd : dd.fields) {
+        final int fieldNumOff = Arrays.binarySearch(fieldNums, fd.fieldNum);
+        assert fieldNumOff >= 0;
+        if (fieldFlags[fieldNumOff] == -1) {
+          fieldFlags[fieldNumOff] = fd.flags;
+        } else if (fieldFlags[fieldNumOff] != fd.flags) {
+          nonChangingFlags = false;
+          break outer;
+        }
+      }
+    }
+
+    if (nonChangingFlags) {
+      // write one flag per field num
+      vectorsStream.writeVInt(0);
+      final PackedInts.Writer writer = PackedInts.getWriterNoHeader(vectorsStream, PackedInts.Format.PACKED, fieldFlags.length, FLAGS_BITS, 1);
+      for (int flags : fieldFlags) {
+        assert flags >= 0;
+        writer.add(flags);
+      }
+      assert writer.ord() == fieldFlags.length - 1;
+      writer.finish();
+    } else {
+      // write one flag for every field instance
+      vectorsStream.writeVInt(1);
+      final PackedInts.Writer writer = PackedInts.getWriterNoHeader(vectorsStream, PackedInts.Format.PACKED, totalFields, FLAGS_BITS, 1);
+      for (DocData dd : pendingDocs) {
+        for (FieldData fd : dd.fields) {
+          writer.add(fd.flags);
+        }
+      }
+      assert writer.ord() == totalFields - 1;
+      writer.finish();
+    }
+  }
+
+  private void flushNumTerms(int totalFields) throws IOException {
+    int maxNumTerms = 0;
+    for (DocData dd : pendingDocs) {
+      for (FieldData fd : dd.fields) {
+        maxNumTerms |= fd.numTerms;
+      }
+    }
+    final int bitsRequired = PackedInts.bitsRequired(maxNumTerms);
+    vectorsStream.writeVInt(bitsRequired);
+    final PackedInts.Writer writer = PackedInts.getWriterNoHeader(
+        vectorsStream, PackedInts.Format.PACKED, totalFields, bitsRequired, 1);
+    for (DocData dd : pendingDocs) {
+      for (FieldData fd : dd.fields) {
+        writer.add(fd.numTerms);
+      }
+    }
+    assert writer.ord() == totalFields - 1;
+    writer.finish();
+  }
+
+  private void flushTermLengths() throws IOException {
+    writer.reset(vectorsStream);
+    for (DocData dd : pendingDocs) {
+      for (FieldData fd : dd.fields) {
+        for (int i = 0; i < fd.numTerms; ++i) {
+          writer.add(fd.prefixLengths[i]);
+        }
+      }
+    }
+    writer.finish();
+    writer.reset(vectorsStream);
+    for (DocData dd : pendingDocs) {
+      for (FieldData fd : dd.fields) {
+        for (int i = 0; i < fd.numTerms; ++i) {
+          writer.add(fd.suffixLengths[i]);
+        }
+      }
+    }
+    writer.finish();
+  }
+
+  private void flushTermFreqs() throws IOException {
+    writer.reset(vectorsStream);
+    for (DocData dd : pendingDocs) {
+      for (FieldData fd : dd.fields) {
+        for (int i = 0; i < fd.numTerms; ++i) {
+          writer.add(fd.freqs[i] - 1);
+        }
+      }
+    }
+    writer.finish();
+  }
+
+  private void flushPositions() throws IOException {
+    writer.reset(vectorsStream);
+    for (DocData dd : pendingDocs) {
+      for (FieldData fd : dd.fields) {
+        if (fd.hasPositions) {
+          int pos = 0;
+          for (int i = 0; i < fd.numTerms; ++i) {
+            int previousPosition = 0;
+            for (int j = 0; j < fd.freqs[i]; ++j) {
+              final int position = positionsBuf[fd .posStart + pos++];
+              writer.add(position - previousPosition);
+              previousPosition = position;
+            }
+          }
+          assert pos == fd.totalPositions;
+        }
+      }
+    }
+    writer.finish();
+  }
+
+  private void flushOffsets(int[] fieldNums) throws IOException {
+    boolean hasOffsets = false;
+    long[] sumPos = new long[fieldNums.length];
+    long[] sumOffsets = new long[fieldNums.length];
+    for (DocData dd : pendingDocs) {
+      for (FieldData fd : dd.fields) {
+        hasOffsets |= fd.hasOffsets;
+        if (fd.hasOffsets && fd.hasPositions) {
+          final int fieldNumOff = Arrays.binarySearch(fieldNums, fd.fieldNum);
+          int pos = 0;
+          for (int i = 0; i < fd.numTerms; ++i) {
+            int previousPos = 0;
+            int previousOff = 0;
+            for (int j = 0; j < fd.freqs[i]; ++j) {
+              final int position = positionsBuf[fd.posStart + pos];
+              final int startOffset = startOffsetsBuf[fd.offStart + pos];
+              sumPos[fieldNumOff] += position - previousPos;
+              sumOffsets[fieldNumOff] += startOffset - previousOff;
+              previousPos = position;
+              previousOff = startOffset;
+              ++pos;
+            }
+          }
+          assert pos == fd.totalPositions;
+        }
+      }
+    }
+
+    if (!hasOffsets) {
+      // nothing to do
+      return;
+    }
+
+    final float[] charsPerTerm = new float[fieldNums.length];
+    for (int i = 0; i < fieldNums.length; ++i) {
+      charsPerTerm[i] = (sumPos[i] <= 0 || sumOffsets[i] <= 0) ? 0 : (float) ((double) sumOffsets[i] / sumPos[i]);
+    }
+
+    // start offsets
+    for (int i = 0; i < fieldNums.length; ++i) {
+      vectorsStream.writeInt(Float.floatToRawIntBits(charsPerTerm[i]));
+    }
+
+    writer.reset(vectorsStream);
+    for (DocData dd : pendingDocs) {
+      for (FieldData fd : dd.fields) {
+        if ((fd.flags & OFFSETS) != 0) {
+          final int fieldNumOff = Arrays.binarySearch(fieldNums, fd.fieldNum);
+          final float cpt = charsPerTerm[fieldNumOff];
+          int pos = 0;
+          for (int i = 0; i < fd.numTerms; ++i) {
+            int previousPos = 0;
+            int previousOff = 0;
+            for (int j = 0; j < fd.freqs[i]; ++j) {
+              final int position = fd.hasPositions ? positionsBuf[fd.posStart + pos] : 0;
+              final int startOffset = startOffsetsBuf[fd.offStart + pos];
+              writer.add(startOffset - previousOff - (int) (cpt * (position - previousPos)));
+              previousPos = position;
+              previousOff = startOffset;
+              ++pos;
+            }
+          }
+        }
+      }
+    }
+    writer.finish();
+
+    // lengths
+    writer.reset(vectorsStream);
+    for (DocData dd : pendingDocs) {
+      for (FieldData fd : dd.fields) {
+        if ((fd.flags & OFFSETS) != 0) {
+          int pos = 0;
+          for (int i = 0; i < fd.numTerms; ++i) {
+            for (int j = 0; j < fd.freqs[i]; ++j) {
+              writer.add(lengthsBuf[fd.offStart + pos++] - fd.prefixLengths[i] - fd.suffixLengths[i]);
+            }
+          }
+          assert pos == fd.totalPositions;
+        }
+      }
+    }
+    writer.finish();
+  }
+
+  private void flushPayloadLengths() throws IOException {
+    writer.reset(vectorsStream);
+    for (DocData dd : pendingDocs) {
+      for (FieldData fd : dd.fields) {
+        if (fd.hasPayloads) {
+          for (int i = 0; i < fd.totalPositions; ++i) {
+            writer.add(payloadLengthsBuf[fd.payStart + i]);
+          }
+        }
+      }
+    }
+    writer.finish();
+  }
+
+  @Override
+  public void finish(FieldInfos fis, int numDocs) throws IOException {
+    if (!pendingDocs.isEmpty()) {
+      flush();
+      numDirtyChunks++; // incomplete: we had to force this flush
+    }
+    if (numDocs != this.numDocs) {
+      throw new RuntimeException("Wrote " + this.numDocs + " docs, finish called with numDocs=" + numDocs);
+    }
+    // Ensure that the encrypted attribute is added to the field infos
+    for (FieldInfo fi : fis) {
+      if (parentFormat.isFieldEncrypted(fi.name)) {
+        fi.putAttribute(CipherFactory.ENCRYPTED_FIELD_KEY, Boolean.toString(true));
+      }
+    }
+    indexWriter.finish(numDocs, vectorsStream.getFilePointer());
+    vectorsStream.writeVLong(numChunks);
+    vectorsStream.writeVLong(numDirtyChunks);
+    CodecUtil.writeFooter(vectorsStream);
+  }
+
+  @Override
+  public void addProx(int numProx, DataInput positions, DataInput offsets)
+      throws IOException {
+    assert (curField.hasPositions) == (positions != null);
+    assert (curField.hasOffsets) == (offsets != null);
+
+    if (curField.hasPositions) {
+      final int posStart = curField.posStart + curField.totalPositions;
+      if (posStart + numProx > positionsBuf.length) {
+        positionsBuf = ArrayUtil.grow(positionsBuf, posStart + numProx);
+      }
+      int position = 0;
+      if (curField.hasPayloads) {
+        final int payStart = curField.payStart + curField.totalPositions;
+        if (payStart + numProx > payloadLengthsBuf.length) {
+          payloadLengthsBuf = ArrayUtil.grow(payloadLengthsBuf, payStart + numProx);
+        }
+        for (int i = 0; i < numProx; ++i) {
+          final int code = positions.readVInt();
+          if ((code & 1) != 0) {
+            // This position has a payload
+            final int payloadLength = positions.readVInt();
+            payloadLengthsBuf[payStart + i] = payloadLength;
+            if (!curField.isEncrypted) {
+              payloadBytes.copyBytes(positions, payloadLength);
+            }
+            else {
+              encryptedPayloadBytes.copyBytes(positions, payloadLength);
+            }
+          } else {
+            payloadLengthsBuf[payStart + i] = 0;
+          }
+          position += code >>> 1;
+          positionsBuf[posStart + i] = position;
+        }
+      } else {
+        for (int i = 0; i < numProx; ++i) {
+          position += (positions.readVInt() >>> 1);
+          positionsBuf[posStart + i] = position;
+        }
+      }
+    }
+
+    if (curField.hasOffsets) {
+      final int offStart = curField.offStart + curField.totalPositions;
+      if (offStart + numProx > startOffsetsBuf.length) {
+        final int newLength = ArrayUtil.oversize(offStart + numProx, 4);
+        startOffsetsBuf = Arrays.copyOf(startOffsetsBuf, newLength);
+        lengthsBuf = Arrays.copyOf(lengthsBuf, newLength);
+      }
+      int lastOffset = 0, startOffset, endOffset;
+      for (int i = 0; i < numProx; ++i) {
+        startOffset = lastOffset + offsets.readVInt();
+        endOffset = startOffset + offsets.readVInt();
+        lastOffset = endOffset;
+        startOffsetsBuf[offStart + i] = startOffset;
+        lengthsBuf[offStart + i] = endOffset - startOffset;
+      }
+    }
+
+    curField.totalPositions += numProx;
+  }
+  
+  // bulk merge is scary: its caused corruption bugs in the past.
+  // we try to be extra safe with this impl, but add an escape hatch to
+  // have a workaround for undiscovered bugs.
+  static final String BULK_MERGE_ENABLED_SYSPROP = EncryptedCompressingTermVectorsWriter.class.getName() + ".enableBulkMerge";
+  static final boolean BULK_MERGE_ENABLED;
+  static {
+    boolean v = true;
+    try {
+      v = Boolean.parseBoolean(System.getProperty(BULK_MERGE_ENABLED_SYSPROP, "true"));
+    } catch (SecurityException ignored) {}
+    BULK_MERGE_ENABLED = v;
+  }
+
+  @Override
+  public int merge(MergeState mergeState) throws IOException {
+    int docCount = 0;
+    int numReaders = mergeState.maxDocs.length;
+
+    // Add the encrypted attribute to the merged field infos (they will be used by the
+    // EncryptedMatchingReaders).
+    for (FieldInfo mergeFi : mergeState.mergeFieldInfos) {
+      if (parentFormat.isFieldEncrypted(mergeFi.name)) {
+        mergeFi.putAttribute(CipherFactory.ENCRYPTED_FIELD_KEY, Boolean.toString(true));
+      }
+    }
+
+    EncryptedMatchingReaders matching = new EncryptedMatchingReaders(mergeState);
+    
+    for (int readerIndex=0;readerIndex<numReaders;readerIndex++) {
+      EncryptedCompressingTermVectorsReader matchingVectorsReader = null;
+      final TermVectorsReader vectorsReader = mergeState.termVectorsReaders[readerIndex];
+      if (matching.matchingReaders[readerIndex]) {
+        // we can only bulk-copy if the matching reader is also a EncryptedCompressingTermVectorsReader
+        if (vectorsReader != null && vectorsReader instanceof EncryptedCompressingTermVectorsReader) {
+          matchingVectorsReader = (EncryptedCompressingTermVectorsReader) vectorsReader;
+        }
+      }
+
+      final int maxDoc = mergeState.maxDocs[readerIndex];
+      final Bits liveDocs = mergeState.liveDocs[readerIndex];
+      
+      if (matchingVectorsReader != null &&
+          matchingVectorsReader.getCompressionMode() == compressionMode &&
+          matchingVectorsReader.getChunkSize() == chunkSize &&
+          matchingVectorsReader.getVersion() == VERSION_CURRENT && 
+          matchingVectorsReader.getPackedIntsVersion() == PackedInts.VERSION_CURRENT &&
+          matchingVectorsReader.getCipherVersion() == encipherVersion &&
+          BULK_MERGE_ENABLED &&
+          liveDocs == null &&
+          !tooDirty(matchingVectorsReader)) {
+        // optimized merge, raw byte copy
+        // its not worth fine-graining this if there are deletions.
+        
+        matchingVectorsReader.checkIntegrity();
+        
+        // flush any pending chunks
+        if (!pendingDocs.isEmpty()) {
+          flush();
+          numDirtyChunks++; // incomplete: we had to force this flush
+        }
+        
+        // iterate over each chunk. we use the vectors index to find chunk boundaries,
+        // read the docstart + doccount from the chunk header (we write a new header, since doc numbers will change),
+        // and just copy the bytes directly.
+        IndexInput rawDocs = matchingVectorsReader.getVectorsStream();
+        CompressingStoredFieldsIndexReader index = matchingVectorsReader.getIndexReader();
+        rawDocs.seek(index.getStartPointer(0));
+        int docID = 0;
+        while (docID < maxDoc) {
+          // read header
+          int base = rawDocs.readVInt();
+          if (base != docID) {
+            throw new CorruptIndexException("invalid state: base=" + base + ", docID=" + docID, rawDocs);
+          }
+          int bufferedDocs = rawDocs.readVInt();
+          
+          // write a new index entry and new header for this chunk.
+          indexWriter.writeIndex(bufferedDocs, vectorsStream.getFilePointer());
+          vectorsStream.writeVInt(docCount); // rebase
+          vectorsStream.writeVInt(bufferedDocs);
+          docID += bufferedDocs;
+          docCount += bufferedDocs;
+          numDocs += bufferedDocs;
+          
+          if (docID > maxDoc) {
+            throw new CorruptIndexException("invalid state: base=" + base + ", count=" + bufferedDocs + ", maxDoc=" + maxDoc, rawDocs);
+          }
+          
+          // copy bytes until the next chunk boundary (or end of chunk data).
+          // using the stored fields index for this isn't the most efficient, but fast enough
+          // and is a source of redundancy for detecting bad things.
+          final long end;
+          if (docID == maxDoc) {
+            end = matchingVectorsReader.getMaxPointer();
+          } else {
+            end = index.getStartPointer(docID);
+          }
+          vectorsStream.copyBytes(rawDocs, end - rawDocs.getFilePointer());
+        }
+               
+        if (rawDocs.getFilePointer() != matchingVectorsReader.getMaxPointer()) {
+          throw new CorruptIndexException("invalid state: pos=" + rawDocs.getFilePointer() + ", max=" + matchingVectorsReader.getMaxPointer(), rawDocs);
+        }
+        
+        // since we bulk merged all chunks, we inherit any dirty ones from this segment.
+        numChunks += matchingVectorsReader.getNumChunks();
+        numDirtyChunks += matchingVectorsReader.getNumDirtyChunks();
+      } else {        
+        // naive merge...
+        if (vectorsReader != null) {
+          vectorsReader.checkIntegrity();
+        }
+        for (int i = 0; i < maxDoc; i++) {
+          if (liveDocs != null && liveDocs.get(i) == false) {
+            continue;
+          }
+          Fields vectors;
+          if (vectorsReader == null) {
+            vectors = null;
+          } else {
+            vectors = vectorsReader.get(i);
+          }
+          addAllDocVectors(vectors, mergeState);
+          ++docCount;
+        }
+      }
+    }
+    finish(mergeState.mergeFieldInfos, docCount);
+    return docCount;
+  }
+
+  /** 
+   * Returns true if we should recompress this reader, even though we could bulk merge compressed data 
+   * <p>
+   * The last chunk written for a segment is typically incomplete, so without recompressing,
+   * in some worst-case situations (e.g. frequent reopen with tiny flushes), over time the 
+   * compression ratio can degrade. This is a safety switch.
+   */
+  boolean tooDirty(EncryptedCompressingTermVectorsReader candidate) {
+    // more than 1% dirty, or more than hard limit of 1024 dirty chunks
+    return candidate.getNumDirtyChunks() > 1024 || 
+           candidate.getNumDirtyChunks() * 100 > candidate.getNumChunks();
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedMatchingReaders.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedMatchingReaders.java
new file mode 100644
index 0000000..24ac8b2
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/compressing/EncryptedMatchingReaders.java
@@ -0,0 +1,82 @@
+package org.apache.lucene.codecs.encrypted.compressing;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.encrypted.CipherFactory;
+import org.apache.lucene.index.FieldInfo;
+import org.apache.lucene.index.MergeState;
+import org.apache.lucene.index.SegmentReader;
+
+/** 
+ * Computes which segments have identical field name to number mappings, and identical
+ * encrypted fields, which allows stored fields and term vectors in this codec to be bulk-merged.
+ */
+class EncryptedMatchingReaders {
+  
+  /** {@link SegmentReader}s that have identical field
+   * name/number mapping, so their stored fields and term
+   * vectors may be bulk merged. */
+  final boolean[] matchingReaders;
+
+  /** How many {@link #matchingReaders} are set. */
+  final int count;
+  
+  EncryptedMatchingReaders(MergeState mergeState) {
+    // If the i'th reader is a SegmentReader and has
+    // identical fieldName -> number mapping, then this
+    // array will be non-null at position i:
+    int numReaders = mergeState.maxDocs.length;
+    int matchedCount = 0;
+    matchingReaders = new boolean[numReaders];
+
+    // If this reader is a SegmentReader, and (1) all of its
+    // field name -> number mappings match the "merged"
+    // FieldInfos, (2) all of its encrypted fields are also
+    // encrypted in the "merged" FieldInfos,
+    // then we can do a bulk copy of the stored fields:
+
+    nextReader:
+    for (int i = 0; i < numReaders; i++) {
+      for (FieldInfo fi : mergeState.fieldInfos[i]) {
+        FieldInfo other = mergeState.mergeFieldInfos.fieldInfo(fi.number);
+        if (other == null || !other.name.equals(fi.name)) {
+          continue nextReader;
+        }
+
+        String encryptedKeyValue = fi.getAttribute(CipherFactory.ENCRYPTED_FIELD_KEY);
+        boolean isEncrypted = encryptedKeyValue != null ? Boolean.parseBoolean(encryptedKeyValue) : false;
+        encryptedKeyValue = other.getAttribute(CipherFactory.ENCRYPTED_FIELD_KEY);
+        boolean isOtherEncrypted = encryptedKeyValue != null ? Boolean.parseBoolean(encryptedKeyValue) : false;
+        if (isOtherEncrypted != isEncrypted) {
+          continue nextReader;
+        }
+      }
+      matchingReaders[i] = true;
+      matchedCount++;
+    }
+    
+    this.count = matchedCount;
+
+    if (mergeState.infoStream.isEnabled("SM")) {
+      mergeState.infoStream.message("SM", "merge store matchedCount=" + count + " vs " + numReaders);
+      if (count != numReaders) {
+        mergeState.infoStream.message("SM", "" + (numReaders - count) + " non-bulk merges");
+      }
+    }
+  }
+}
diff --git lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/package-info.java lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/package-info.java
new file mode 100644
index 0000000..f8e9e2c
--- /dev/null
+++ lucene/codecs/src/java/org/apache/lucene/codecs/encrypted/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Codec for index-level encryption.
+ * <p>
+ * Pre-requisite: The Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files must be installed
+ * on the system.
+ */
+package org.apache.lucene.codecs.encrypted;
diff --git lucene/codecs/src/resources/META-INF/services/org.apache.lucene.codecs.Codec lucene/codecs/src/resources/META-INF/services/org.apache.lucene.codecs.Codec
index fcd5ded..38200ee 100644
--- lucene/codecs/src/resources/META-INF/services/org.apache.lucene.codecs.Codec
+++ lucene/codecs/src/resources/META-INF/services/org.apache.lucene.codecs.Codec
@@ -14,3 +14,4 @@
 #  limitations under the License.
 
 org.apache.lucene.codecs.simpletext.SimpleTextCodec
+org.apache.lucene.codecs.encrypted.DummyEncryptedLucene60Codec
\ No newline at end of file
diff --git lucene/codecs/src/resources/META-INF/services/org.apache.lucene.codecs.PostingsFormat lucene/codecs/src/resources/META-INF/services/org.apache.lucene.codecs.PostingsFormat
index b82f156..517ce16 100644
--- lucene/codecs/src/resources/META-INF/services/org.apache.lucene.codecs.PostingsFormat
+++ lucene/codecs/src/resources/META-INF/services/org.apache.lucene.codecs.PostingsFormat
@@ -21,3 +21,4 @@ org.apache.lucene.codecs.memory.FSTPostingsFormat
 org.apache.lucene.codecs.memory.MemoryPostingsFormat
 org.apache.lucene.codecs.simpletext.SimpleTextPostingsFormat
 org.apache.lucene.codecs.autoprefix.AutoPrefixPostingsFormat
+org.apache.lucene.codecs.encrypted.DummyEncryptedLucene60Codec$DummyEncryptedLucene50PostingsFormat
diff --git lucene/codecs/src/test/META-INF/services/org.apache.lucene.codecs.Codec lucene/codecs/src/test/META-INF/services/org.apache.lucene.codecs.Codec
new file mode 100644
index 0000000..46bcdff
--- /dev/null
+++ lucene/codecs/src/test/META-INF/services/org.apache.lucene.codecs.Codec
@@ -0,0 +1,18 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.apache.lucene.codecs.encrypted.RandomEncryptedLucene60Codec
\ No newline at end of file
diff --git lucene/codecs/src/test/META-INF/services/org.apache.lucene.codecs.PostingsFormat lucene/codecs/src/test/META-INF/services/org.apache.lucene.codecs.PostingsFormat
new file mode 100644
index 0000000..27954f9
--- /dev/null
+++ lucene/codecs/src/test/META-INF/services/org.apache.lucene.codecs.PostingsFormat
@@ -0,0 +1,18 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.apache.lucene.codecs.encrypted.RandomEncryptedLucene60Codec$RandomEncryptedLucene50PostingsFormat
diff --git lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/RandomCipherFactory.java lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/RandomCipherFactory.java
new file mode 100644
index 0000000..fd12d83
--- /dev/null
+++ lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/RandomCipherFactory.java
@@ -0,0 +1,105 @@
+package org.apache.lucene.codecs.encrypted;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+class RandomCipherFactory implements CipherFactory {
+
+  private final Random r;
+  private int currentVersion = -1;
+  private final List<byte[]> aesKeys;
+  private final List<byte[]> ivs;
+
+  RandomCipherFactory(long seed) {
+    this.r = new Random(seed);
+    this.aesKeys = new ArrayList<>();
+    this.ivs = new ArrayList<>();
+    this.incrementVersion(); // generate key for version 0
+  }
+
+  private byte[] newAESKey() {
+    byte[] aes = new byte[32];
+    r.nextBytes(aes);
+    return aes;
+  }
+
+  private byte[] newIV() {
+    byte[] iv = new byte[16];
+    r.nextBytes(iv);
+    return iv;
+  }
+
+  public void incrementVersion() {
+    currentVersion++;
+    aesKeys.add(this.newAESKey());
+    ivs.add(this.newIV());
+  }
+
+  @Override
+  public Cipher newEncipherInstance() {
+    return newEncipherInstance(getEncipherVersion());
+  }
+
+  private Cipher newEncipherInstance(long version) {
+    try {
+      //AESKey
+      SecretKeySpec aesKeySpec = new SecretKeySpec(this.aesKeys.get((int) version), "AES");
+      //Initialization Vector
+      IvParameterSpec ips = new IvParameterSpec(this.ivs.get((int) version));
+      //Cipher
+      Cipher aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+      aesCipher.init(Cipher.ENCRYPT_MODE, aesKeySpec, ips);
+      return aesCipher;
+    } catch (Exception e) {
+      throw new CipherFactoryException("Cannot instantiate encipher instance", e);
+    }
+  }
+
+  @Override
+  public long getEncipherVersion() {
+    return currentVersion;
+  }
+
+  @Override
+  public Cipher newDecipherInstance(long version) {
+    // Generates the required key version
+    while (version > currentVersion) {
+      this.incrementVersion();
+    }
+
+    try {
+      //AESKey
+      SecretKeySpec aesKeySpec = new SecretKeySpec(this.aesKeys.get((int) version), "AES");
+      //Initialization Vector
+      IvParameterSpec ips = new IvParameterSpec(this.ivs.get((int) version));
+      //Cipher
+      Cipher aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+      aesCipher.init(Cipher.DECRYPT_MODE, aesKeySpec, ips);
+      return aesCipher;
+    } catch (Exception e) {
+      throw new CipherFactoryException("Cannot instantiate decipher instance", e);
+    }
+  }
+
+}
\ No newline at end of file
diff --git lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/RandomEncryptedLucene60Codec.java lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/RandomEncryptedLucene60Codec.java
new file mode 100644
index 0000000..3581d34
--- /dev/null
+++ lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/RandomEncryptedLucene60Codec.java
@@ -0,0 +1,193 @@
+package org.apache.lucene.codecs.encrypted;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import com.carrotsearch.randomizedtesting.RandomizedTest;
+import org.apache.lucene.codecs.PostingsFormat;
+import org.apache.lucene.codecs.StoredFieldsFormat;
+import org.apache.lucene.codecs.TermVectorsFormat;
+import org.apache.lucene.codecs.compressing.CompressionMode;
+import org.apache.lucene.codecs.encrypted.compressing.EncryptedCompressingStoredFieldsFormat;
+import org.apache.lucene.codecs.lucene50.Lucene50PostingsFormat;
+import org.apache.lucene.util.BytesRef;
+
+import java.util.Random;
+
+/**
+ * A {@link EncryptedLucene60Codec} that randomly encrypts fields.
+ */
+public class RandomEncryptedLucene60Codec extends EncryptedLucene60Codec {
+
+  private final EncryptedLucene50PostingsFormat encryptedPostingsFormat;
+  private final Lucene50PostingsFormat postingsFormat;
+  private final EncryptedLucene50StoredFieldsFormat storedFieldsFormat;
+  private final EncryptedLucene50TermVectorsFormat vectorsFormat;
+
+  private final RandomCipherFactory cipherFactory = new RandomCipherFactory(SEED);
+  private final Random r = new Random();
+
+  private static final int SEED = RandomizedTest.getRandom().nextInt();
+
+  private static final EncryptedLucene50StoredFieldsFormat.Mode DEFAULT_MODE = EncryptedLucene50StoredFieldsFormat.Mode.BEST_SPEED;
+
+  public RandomEncryptedLucene60Codec() {
+    this.postingsFormat = new Lucene50PostingsFormat();
+    this.encryptedPostingsFormat = new RandomEncryptedLucene50PostingsFormat(cipherFactory);
+    this.storedFieldsFormat = new RandomEncryptedLucene50StoredFieldsFormat(cipherFactory);
+    this.vectorsFormat = new RandomEncryptedLucene50TermVectorsFormat(cipherFactory);
+  }
+
+  @Override
+  public RandomCipherFactory getCipherFactory() {
+    return cipherFactory;
+  }
+
+  @Override
+  public PostingsFormat getPostingsFormatForField(String field) {
+    r.setSeed(2^32 * new BytesRef(field).hashCode() + SEED);
+    boolean isEncrypted = r.nextBoolean();
+    if (isEncrypted) {
+      return encryptedPostingsFormat;
+    }
+    else {
+      return postingsFormat;
+    }
+  }
+
+  @Override
+  public EncryptedLucene50StoredFieldsFormat storedFieldsFormat() {
+    return storedFieldsFormat;
+  }
+
+  @Override
+  public TermVectorsFormat termVectorsFormat() {
+    return vectorsFormat;
+  }
+
+  public static class RandomEncryptedLucene50PostingsFormat extends EncryptedLucene50PostingsFormat {
+
+    private CipherFactory cipherFactory = new RandomCipherFactory(SEED);
+
+    /**
+     * Default constructor used for reading. It will use the local {@link CipherFactory} instance.
+     */
+    public RandomEncryptedLucene50PostingsFormat() {}
+
+    /**
+     * Constructor to pass the cipher factory instance that will be used for writing.
+     */
+    public RandomEncryptedLucene50PostingsFormat(CipherFactory cipherFactory) {
+      this.cipherFactory = cipherFactory;
+    }
+
+    @Override
+    protected CipherFactory getCipherFactory() {
+      return cipherFactory;
+    }
+
+  }
+
+  public static class RandomEncryptedLucene50StoredFieldsFormat extends EncryptedLucene50StoredFieldsFormat {
+
+    private CipherFactory cipherFactory = new RandomCipherFactory(SEED);
+
+    /**
+     * Default constructor used for reading. It will use the local {@link CipherFactory} instance.
+     */
+    public RandomEncryptedLucene50StoredFieldsFormat() {
+      super(DEFAULT_MODE);
+    }
+
+    /**
+     * Constructor to pass the cipher factory instance that will be used for writing.
+     */
+    public RandomEncryptedLucene50StoredFieldsFormat(CipherFactory cipherFactory) {
+      super(DEFAULT_MODE);
+      this.cipherFactory = cipherFactory;
+    }
+
+    @Override
+    protected CipherFactory getCipherFactory() {
+      return cipherFactory;
+    }
+
+    @Override
+    protected StoredFieldsFormat newEncryptedCompressingStoredFieldsFormat(String formatName, CipherFactory cipherFactory,
+                                                                           CompressionMode compressionMode,
+                                                                           int chunkSize, int maxDocsPerChunk, int blockSize) {
+      return new RandomEncryptedCompressingStoredFieldsFormat(formatName, cipherFactory, compressionMode, chunkSize, maxDocsPerChunk, blockSize);
+    }
+
+    /**
+     * Encrypt randomly a field based on the hash of its name and of the seed.
+     */
+    private static class RandomEncryptedCompressingStoredFieldsFormat extends EncryptedCompressingStoredFieldsFormat {
+
+      private final Random r = new Random();
+
+      public RandomEncryptedCompressingStoredFieldsFormat(String formatName, CipherFactory cipherFactory,
+                                                          CompressionMode compressionMode,
+                                                          int chunkSize, int maxDocsPerChunk, int blockSize) {
+        super(formatName, cipherFactory, compressionMode, chunkSize, maxDocsPerChunk, blockSize);
+      }
+
+      @Override
+      public boolean isFieldEncrypted(String field) {
+        r.setSeed(2^32 * new BytesRef(field).hashCode() + SEED);
+        boolean isEncrypted = r.nextBoolean();
+        return isEncrypted;
+      }
+
+    }
+
+  }
+
+  public static class RandomEncryptedLucene50TermVectorsFormat extends EncryptedLucene50TermVectorsFormat {
+
+    private CipherFactory cipherFactory = new RandomCipherFactory(SEED);
+
+    private final Random r = new Random();
+
+    /**
+     * Default constructor used for reading. It will use the local {@link CipherFactory} instance.
+     */
+    public RandomEncryptedLucene50TermVectorsFormat() {}
+
+    /**
+     * Constructor to pass the cipher factory instance that will be used for writing.
+     */
+    public RandomEncryptedLucene50TermVectorsFormat(CipherFactory cipherFactory) {
+      super();
+      this.cipherFactory = cipherFactory;
+    }
+
+    @Override
+    public CipherFactory getCipherFactory() {
+      return cipherFactory;
+    }
+
+    @Override
+    public boolean isFieldEncrypted(String field) {
+      r.setSeed(2^32 * new BytesRef(field).hashCode() + SEED);
+      boolean isEncrypted = r.nextBoolean();
+      return isEncrypted;
+    }
+
+  }
+
+}
diff --git lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/TestEncryptedLucene50PostingsFormat.java lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/TestEncryptedLucene50PostingsFormat.java
new file mode 100644
index 0000000..460a5aa
--- /dev/null
+++ lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/TestEncryptedLucene50PostingsFormat.java
@@ -0,0 +1,118 @@
+package org.apache.lucene.codecs.encrypted;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.Codec;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.index.BasePostingsFormatTestCase;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.store.Directory;
+
+/**
+ * Tests EncryptedLucene50's postings
+ */
+public class TestEncryptedLucene50PostingsFormat extends BasePostingsFormatTestCase {
+
+  private final RandomEncryptedLucene60Codec codec = new RandomEncryptedLucene60Codec();
+
+  @Override
+  protected Codec getCodec() {
+    return codec;
+  }
+
+  /**
+   * Creates multiple segments, each one with a different key version, then checks
+   * that we can read them properly.
+   */
+  public void testDifferentKeyVersions() throws Exception {
+    try (Directory dir = newDirectory()) {
+
+      // Index a first batch of documents with key version 0
+      IndexWriterConfig iwc = newIndexWriterConfig(null);
+      iwc.setCodec(codec);
+      try (RandomIndexWriter iw = new RandomIndexWriter(random(), dir, iwc)) {
+        for (int i = 0; i < 10; i++) {
+          Document doc = new Document();
+          doc.add(newStringField("id", Integer.toString(i), Field.Store.NO));
+          iw.addDocument(doc);
+        }
+      }
+
+      // increment key version
+      codec.getCipherFactory().incrementVersion();
+
+      // Index a second batch of documents with key version 1
+      iwc = newIndexWriterConfig(null);
+      iwc.setCodec(codec);
+      try (RandomIndexWriter iw = new RandomIndexWriter(random(), dir, iwc)) {
+        for (int i = 10; i < 20; i++) {
+          Document doc = new Document();
+          doc.add(newStringField("id", Integer.toString(i), Field.Store.NO));
+          iw.addDocument(doc);
+        }
+      }
+
+      // increment key version
+      codec.getCipherFactory().incrementVersion();
+
+      // Index a third batch of documents with key version 2
+      iwc = newIndexWriterConfig(null);
+      iwc.setCodec(codec);
+      try (RandomIndexWriter iw = new RandomIndexWriter(random(), dir, iwc)) {
+        for (int i = 20; i < 30; i++) {
+          Document doc = new Document();
+          doc.add(newStringField("id", Integer.toString(i), Field.Store.NO));
+          iw.addDocument(doc);
+        }
+      }
+
+      // Reader should be able to read the three segments
+      try (DirectoryReader ir = DirectoryReader.open(dir)) {
+        IndexSearcher searcher = new IndexSearcher(ir);
+        assertEquals(1, searcher.count(new TermQuery(new Term("id", "1"))));
+        assertEquals(1, searcher.count(new TermQuery(new Term("id", "11"))));
+        assertEquals(1, searcher.count(new TermQuery(new Term("id", "21"))));
+      }
+
+      // increment key version
+      codec.getCipherFactory().incrementVersion();
+
+      // Merge the segments with key version 3
+      iwc = newIndexWriterConfig(null);
+      iwc.setCodec(codec);
+      try (RandomIndexWriter iw = new RandomIndexWriter(random(), dir, iwc)) {
+        iw.forceMerge(1);
+      }
+
+      // Reader should be able to read the new segment
+      try (DirectoryReader ir = DirectoryReader.open(dir)) {
+        IndexSearcher searcher = new IndexSearcher(ir);
+        assertEquals(1, searcher.count(new TermQuery(new Term("id", "1"))));
+        assertEquals(1, searcher.count(new TermQuery(new Term("id", "11"))));
+        assertEquals(1, searcher.count(new TermQuery(new Term("id", "21"))));
+      }
+    }
+  }
+
+}
diff --git lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/TestEncryptedLucene50TermVectorsFormat.java lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/TestEncryptedLucene50TermVectorsFormat.java
new file mode 100644
index 0000000..7b2454b
--- /dev/null
+++ lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/TestEncryptedLucene50TermVectorsFormat.java
@@ -0,0 +1,146 @@
+package org.apache.lucene.codecs.encrypted;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.MockTokenizer;
+import org.apache.lucene.codecs.Codec;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.FieldType;
+import org.apache.lucene.document.TextField;
+import org.apache.lucene.index.BaseTermVectorsFormatTestCase;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.Fields;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.index.Terms;
+import org.apache.lucene.index.TermsEnum;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.BytesRef;
+
+public class TestEncryptedLucene50TermVectorsFormat extends BaseTermVectorsFormatTestCase {
+
+  private final RandomEncryptedLucene60Codec codec = new RandomEncryptedLucene60Codec();
+
+  @Override
+  protected Codec getCodec() {
+    return codec;
+  }
+
+  /**
+   * Creates multiple segments, each one with a different key version, then checks
+   * that we can read them properly.
+   */
+  public void testDifferentKeyVersions() throws Exception {
+    try (Directory dir = newDirectory()) {
+
+      FieldType ft = new FieldType(TextField.TYPE_NOT_STORED);
+      ft.setStoreTermVectors(true);
+      ft.setStoreTermVectorPositions(true);
+      ft.setStoreTermVectorOffsets(true);
+
+      // Index a first batch of documents with key version 0
+      try (RandomIndexWriter iw = new RandomIndexWriter(random(), dir, newIWConfig())) {
+        for (int i = 0; i < 10; i++) {
+          Document doc = new Document();
+          doc.add(newStringField("id", Integer.toString(i), Field.Store.YES));
+          doc.add(new Field("foo", "bar bar", ft));
+          iw.addDocument(doc);
+        }
+      }
+
+      // increment key version
+      codec.getCipherFactory().incrementVersion();
+
+      // Index a second batch of documents with key version 1
+      try (RandomIndexWriter iw = new RandomIndexWriter(random(), dir, newIWConfig())) {
+        for (int i = 10; i < 20; i++) {
+          Document doc = new Document();
+          doc.add(newStringField("id", Integer.toString(i), Field.Store.YES));
+          doc.add(new Field("foo", "bar bar", ft));
+          iw.addDocument(doc);
+        }
+      }
+
+      // increment key version
+      codec.getCipherFactory().incrementVersion();
+
+      // Index a third batch of documents with key version 2
+      try (RandomIndexWriter iw = new RandomIndexWriter(random(), dir, newIWConfig())) {
+        for (int i = 20; i < 30; i++) {
+          Document doc = new Document();
+          doc.add(newStringField("id", Integer.toString(i), Field.Store.YES));
+          doc.add(new Field("foo", "bar bar", ft));
+          iw.addDocument(doc);
+        }
+      }
+
+      // Reader should be able to read the three segments
+      try (DirectoryReader ir = DirectoryReader.open(dir)) {
+        for (int i = 0; i < 30; i++) {
+          IndexSearcher searcher = new IndexSearcher(ir);
+          TopDocs hits = searcher.search(new TermQuery(new Term("id", Integer.toString(i))), 1);
+          assertEquals(1, hits.totalHits);
+          Document doc = ir.document(hits.scoreDocs[0].doc);
+          assertEquals(Integer.toString(i), doc.get("id"));
+        }
+      }
+
+      // increment key version
+      codec.getCipherFactory().incrementVersion();
+
+      // Merge the segments with key version 3
+      try (RandomIndexWriter iw = new RandomIndexWriter(random(), dir, newIWConfig())) {
+        iw.forceMerge(1);
+      }
+
+      // Reader should be able to read the new segment
+      try (DirectoryReader ir = DirectoryReader.open(dir)) {
+        for (int i = 0; i < 30; i++) {
+          IndexSearcher searcher = new IndexSearcher(ir);
+          TopDocs hits = searcher.search(new TermQuery(new Term("id", Integer.toString(i))), 1);
+          assertEquals(1, hits.totalHits);
+          Document doc = ir.document(hits.scoreDocs[0].doc);
+          assertEquals(Integer.toString(i), doc.get("id"));
+          Fields fields = ir.getTermVectors(i);
+          Terms terms = fields.terms("foo");
+          assertTrue(terms.hasPositions() && terms.hasOffsets());
+          TermsEnum termsEnum = terms.iterator();
+          assertEquals(new BytesRef("bar"), termsEnum.next());
+        }
+      }
+    }
+  }
+
+  private IndexWriterConfig newIWConfig() {
+    IndexWriterConfig iwc = new IndexWriterConfig(new Analyzer() {
+      @Override
+      protected TokenStreamComponents createComponents(String fieldName) {
+        return new TokenStreamComponents(new MockTokenizer());
+      }
+    });
+    iwc.setCodec(codec);
+    return iwc;
+  }
+
+}
diff --git lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/TestEncryptedStoredFieldsFormat.java lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/TestEncryptedStoredFieldsFormat.java
new file mode 100644
index 0000000..48ee417
--- /dev/null
+++ lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/TestEncryptedStoredFieldsFormat.java
@@ -0,0 +1,132 @@
+package org.apache.lucene.codecs.encrypted;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.Codec;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.index.BaseStoredFieldsFormatTestCase;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.store.Directory;
+import org.junit.Ignore;
+
+/**
+ * Tests EncryptedLucene50's stored fields.
+ */
+public class TestEncryptedStoredFieldsFormat extends BaseStoredFieldsFormatTestCase {
+
+  private final RandomEncryptedLucene60Codec codec = new RandomEncryptedLucene60Codec();
+
+  @Override
+  protected Codec getCodec() {
+    return codec;
+  }
+
+  @Ignore
+  @Override
+  public void testStoredFieldsOrder() {
+    // Current implementation does not maintain order of stored fields.
+  }
+
+  /**
+   * Creates multiple segments, each one with a different key version, then checks
+   * that we can read them properly.
+   */
+  public void testDifferentKeyVersions() throws Exception {
+    try (Directory dir = newDirectory()) {
+
+      // Index a first batch of documents with key version 0
+      IndexWriterConfig iwc = newIndexWriterConfig(null);
+      iwc.setCodec(codec);
+      try (RandomIndexWriter iw = new RandomIndexWriter(random(), dir, iwc)) {
+        for (int i = 0; i < 10; i++) {
+          Document doc = new Document();
+          doc.add(newStringField("id", Integer.toString(i), Field.Store.YES));
+          iw.addDocument(doc);
+        }
+      }
+
+      // increment key version
+      codec.getCipherFactory().incrementVersion();
+
+      // Index a second batch of documents with key version 1
+      iwc = newIndexWriterConfig(null);
+      iwc.setCodec(codec);
+      try (RandomIndexWriter iw = new RandomIndexWriter(random(), dir, iwc)) {
+        for (int i = 10; i < 20; i++) {
+          Document doc = new Document();
+          doc.add(newStringField("id", Integer.toString(i), Field.Store.YES));
+          iw.addDocument(doc);
+        }
+      }
+
+      // increment key version
+      codec.getCipherFactory().incrementVersion();
+
+      // Index a third batch of documents with key version 2
+      iwc = newIndexWriterConfig(null);
+      iwc.setCodec(codec);
+      try (RandomIndexWriter iw = new RandomIndexWriter(random(), dir, iwc)) {
+        for (int i = 20; i < 30; i++) {
+          Document doc = new Document();
+          doc.add(newStringField("id", Integer.toString(i), Field.Store.YES));
+          iw.addDocument(doc);
+        }
+      }
+
+      // Reader should be able to read the three segments
+      try (DirectoryReader ir = DirectoryReader.open(dir)) {
+        for (int i = 0; i < 30; i++) {
+          IndexSearcher searcher = new IndexSearcher(ir);
+          TopDocs hits = searcher.search(new TermQuery(new Term("id", Integer.toString(i))), 1);
+          assertEquals(1, hits.totalHits);
+          Document doc = ir.document(hits.scoreDocs[0].doc);
+          assertEquals(Integer.toString(i), doc.get("id"));
+        }
+      }
+
+      // increment key version
+      codec.getCipherFactory().incrementVersion();
+
+      // Merge the segments with key version 3
+      iwc = newIndexWriterConfig(null);
+      iwc.setCodec(codec);
+      try (RandomIndexWriter iw = new RandomIndexWriter(random(), dir, iwc)) {
+        iw.forceMerge(1);
+      }
+
+      // Reader should be able to read the new segment
+      try (DirectoryReader ir = DirectoryReader.open(dir)) {
+        for (int i = 0; i < 30; i++) {
+          IndexSearcher searcher = new IndexSearcher(ir);
+          TopDocs hits = searcher.search(new TermQuery(new Term("id", Integer.toString(i))), 1);
+          assertEquals(1, hits.totalHits);
+          Document doc = ir.document(hits.scoreDocs[0].doc);
+          assertEquals(Integer.toString(i), doc.get("id"));
+        }
+      }
+    }
+  }
+
+}
diff --git lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/TestIndexUpgrader.java lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/TestIndexUpgrader.java
new file mode 100644
index 0000000..5c5a17e
--- /dev/null
+++ lucene/codecs/src/test/org/apache/lucene/codecs/encrypted/TestIndexUpgrader.java
@@ -0,0 +1,147 @@
+package org.apache.lucene.codecs.encrypted;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.codecs.Codec;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.index.SegmentCommitInfo;
+import org.apache.lucene.index.SegmentInfos;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.InfoStream;
+import org.apache.lucene.util.LuceneTestCase;
+import org.apache.lucene.util.TestUtil;
+
+import java.io.IOException;
+
+public class TestIndexUpgrader extends LuceneTestCase {
+
+  private final RandomEncryptedLucene60Codec codec = new RandomEncryptedLucene60Codec();
+
+  protected EncryptedLucene60Codec getCodec() {
+    return codec;
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    Codec.setDefault(getCodec());
+  }
+
+  /**
+   * Randomizes the use of some of hte constructor variations
+   */
+  private static IndexUpgrader newIndexUpgrader(Directory dir) {
+    final boolean streamType = random().nextBoolean();
+    final int choice = TestUtil.nextInt(random(), 0, 2);
+    switch (choice) {
+      case 0: return new IndexUpgrader(dir);
+      case 1: return new IndexUpgrader(dir, streamType ? null : InfoStream.NO_OUTPUT, false);
+      case 2: return new IndexUpgrader(dir, newIndexWriterConfig(null), false);
+      default: fail("case statement didn't get updated when random bounds changed");
+    }
+    return null; // never get here
+  }
+
+  private int checkAllSegmentsUpgraded(Directory dir) throws IOException {
+    final SegmentInfos infos = SegmentInfos.readLatestCommit(dir);
+    for (SegmentCommitInfo si : infos) {
+      String value = si.info.getAttribute(CipherFactory.CIPHER_VERSION_KEY);
+      assertNotNull(value);
+      assertEquals(this.getCodec().getCipherFactory().getEncipherVersion(), Long.parseLong(value));
+    }
+    return infos.size();
+  }
+
+  /**
+   * Creates multiple segments, each one with a different key version, then checks
+   * that we can read them properly.
+   */
+  public void testDifferentKeyVersions() throws Exception {
+    try (Directory dir = newDirectory()) {
+
+      // Index a first batch of documents with key version 0
+      IndexWriterConfig iwc = newIndexWriterConfig(null);
+      iwc.setCodec(codec);
+      try (RandomIndexWriter iw = new RandomIndexWriter(random(), dir, iwc)) {
+        for (int i = 0; i < 10; i++) {
+          Document doc = new Document();
+          doc.add(newStringField("id", Integer.toString(i), Field.Store.NO));
+          iw.addDocument(doc);
+        }
+      }
+
+      // increment key version
+      codec.getCipherFactory().incrementVersion();
+
+      // Index a second batch of documents with key version 1
+      iwc = newIndexWriterConfig(null);
+      iwc.setCodec(codec);
+      try (RandomIndexWriter iw = new RandomIndexWriter(random(), dir, iwc)) {
+        for (int i = 10; i < 20; i++) {
+          Document doc = new Document();
+          doc.add(newStringField("id", Integer.toString(i), Field.Store.NO));
+          iw.addDocument(doc);
+        }
+      }
+
+      // increment key version
+      codec.getCipherFactory().incrementVersion();
+
+      // Index a third batch of documents with key version 2
+      iwc = newIndexWriterConfig(null);
+      iwc.setCodec(codec);
+      try (RandomIndexWriter iw = new RandomIndexWriter(random(), dir, iwc)) {
+        for (int i = 20; i < 30; i++) {
+          Document doc = new Document();
+          doc.add(newStringField("id", Integer.toString(i), Field.Store.NO));
+          iw.addDocument(doc);
+        }
+      }
+
+      // Reader should be able to read the three segments
+      try (DirectoryReader ir = DirectoryReader.open(dir)) {
+        IndexSearcher searcher = new IndexSearcher(ir);
+        assertEquals(1, searcher.count(new TermQuery(new Term("id", "1"))));
+        assertEquals(1, searcher.count(new TermQuery(new Term("id", "11"))));
+        assertEquals(1, searcher.count(new TermQuery(new Term("id", "21"))));
+      }
+
+      // Launch the index upgrade
+      newIndexUpgrader(dir).upgrade();
+
+      // Check that all segments have the right cipher version
+      checkAllSegmentsUpgraded(dir);
+
+      // Reader should be able to read the new segments
+      try (DirectoryReader ir = DirectoryReader.open(dir)) {
+        IndexSearcher searcher = new IndexSearcher(ir);
+        assertEquals(1, searcher.count(new TermQuery(new Term("id", "1"))));
+        assertEquals(1, searcher.count(new TermQuery(new Term("id", "11"))));
+        assertEquals(1, searcher.count(new TermQuery(new Term("id", "21"))));
+      }
+    }
+  }
+
+}
