Snap for 15194150 from 031c80a6b6dbf285a2f9c65bd299d239032f11b9 to androidx-javascriptengine-release

Change-Id: I0ddfbc979a72662aea3721e72045570bbef74b78
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4b7c752..22afb9c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -16,7 +16,10 @@
 
 project(icing)
 
-add_definitions("-DICING_REVERSE_JNI_SEGMENTATION=1")
+add_definitions(
+        "-DICING_REVERSE_JNI_SEGMENTATION=1"
+        "-DICING_DISABLE_EIGEN"
+)
 set(VERSION_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/icing/jni.lds")
 set(CMAKE_CXX_STANDARD 17)
 set(CMAKE_SHARED_LINKER_FLAGS
diff --git a/icing/absl_ports/canonical_errors.cc b/icing/absl_ports/canonical_errors.cc
index 1f43505..bf8b1f5 100644
--- a/icing/absl_ports/canonical_errors.cc
+++ b/icing/absl_ports/canonical_errors.cc
@@ -23,6 +23,93 @@
 namespace lib {
 namespace absl_ports {
 
+libtextclassifier3::Status CancelledError(const char* error_message) {
+  return libtextclassifier3::Status(libtextclassifier3::StatusCode::CANCELLED,
+                                    std::string(error_message));
+}
+
+libtextclassifier3::Status UnknownError(const char* error_message) {
+  return libtextclassifier3::Status(libtextclassifier3::StatusCode::UNKNOWN,
+                                    std::string(error_message));
+}
+
+libtextclassifier3::Status InvalidArgumentError(const char* error_message) {
+  return libtextclassifier3::Status(
+      libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+      std::string(error_message));
+}
+
+libtextclassifier3::Status DeadlineExceededError(const char* error_message) {
+  return libtextclassifier3::Status(
+      libtextclassifier3::StatusCode::DEADLINE_EXCEEDED,
+      std::string(error_message));
+}
+
+libtextclassifier3::Status NotFoundError(const char* error_message) {
+  return libtextclassifier3::Status(libtextclassifier3::StatusCode::NOT_FOUND,
+                                    std::string(error_message));
+}
+
+libtextclassifier3::Status AlreadyExistsError(const char* error_message) {
+  return libtextclassifier3::Status(
+      libtextclassifier3::StatusCode::ALREADY_EXISTS, error_message);
+}
+
+libtextclassifier3::Status PermissionDeniedError(const char* error_message) {
+  return libtextclassifier3::Status(
+      libtextclassifier3::StatusCode::PERMISSION_DENIED,
+      std::string(error_message));
+}
+
+libtextclassifier3::Status ResourceExhaustedError(const char* error_message) {
+  return libtextclassifier3::Status(
+      libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED,
+      std::string(error_message));
+}
+
+libtextclassifier3::Status FailedPreconditionError(const char* error_message) {
+  return libtextclassifier3::Status(
+      libtextclassifier3::StatusCode::FAILED_PRECONDITION,
+      std::string(error_message));
+}
+
+libtextclassifier3::Status AbortedError(const char* error_message) {
+  return libtextclassifier3::Status(libtextclassifier3::StatusCode::ABORTED,
+                                    std::string(error_message));
+}
+
+libtextclassifier3::Status OutOfRangeError(const char* error_message) {
+  return libtextclassifier3::Status(
+      libtextclassifier3::StatusCode::OUT_OF_RANGE, std::string(error_message));
+}
+
+libtextclassifier3::Status UnimplementedError(const char* error_message) {
+  return libtextclassifier3::Status(
+      libtextclassifier3::StatusCode::UNIMPLEMENTED,
+      std::string(error_message));
+}
+
+libtextclassifier3::Status InternalError(const char* error_message) {
+  return libtextclassifier3::Status(libtextclassifier3::StatusCode::INTERNAL,
+                                    std::string(error_message));
+}
+
+libtextclassifier3::Status UnavailableError(const char* error_message) {
+  return libtextclassifier3::Status(libtextclassifier3::StatusCode::UNAVAILABLE,
+                                    std::string(error_message));
+}
+
+libtextclassifier3::Status DataLossError(const char* error_message) {
+  return libtextclassifier3::Status(libtextclassifier3::StatusCode::DATA_LOSS,
+                                    std::string(error_message));
+}
+
+libtextclassifier3::Status UnauthenticatedError(const char* error_message) {
+  return libtextclassifier3::Status(
+      libtextclassifier3::StatusCode::UNAUTHENTICATED,
+      std::string(error_message));
+}
+
 libtextclassifier3::Status CancelledError(std::string error_message) {
   return libtextclassifier3::Status(libtextclassifier3::StatusCode::CANCELLED,
                                     std::move(error_message));
diff --git a/icing/absl_ports/canonical_errors.h b/icing/absl_ports/canonical_errors.h
index 2232946..55bdcea 100644
--- a/icing/absl_ports/canonical_errors.h
+++ b/icing/absl_ports/canonical_errors.h
@@ -23,6 +23,38 @@
 namespace lib {
 namespace absl_ports {
 
+// Overload both const char* and std::string to support 2 different callers:
+// `FooError(StrCat("text", a_str, " and ", b_str));` and
+// `FooError("simple text");`
+//
+// - std::string is used for the first call to avoid additional copies (Note:
+//   if using std::string_view, then the caller cannot move the string).
+// - const char* is used for the second call.
+//   - If const char* is not overloaded, then it is still valid for the second
+//     call to use std::string.
+//   - However, in Android the compiler will implicitly compile some conversion
+//     code (from const char* to std::string) **on the caller side** and
+//     increase most of the classes' size by ~1 KB, and the size of libicing.so
+//     will be increased by ~100 KBs.
+//   - From the experiment, overloading both const char* and std::string can
+//     avoid this binary size increase.
+libtextclassifier3::Status CancelledError(const char* error_message);
+libtextclassifier3::Status UnknownError(const char* error_message);
+libtextclassifier3::Status InvalidArgumentError(const char* error_message);
+libtextclassifier3::Status DeadlineExceededError(const char* error_message);
+libtextclassifier3::Status NotFoundError(const char* error_message);
+libtextclassifier3::Status AlreadyExistsError(const char* error_message);
+libtextclassifier3::Status PermissionDeniedError(const char* error_message);
+libtextclassifier3::Status ResourceExhaustedError(const char* error_message);
+libtextclassifier3::Status FailedPreconditionError(const char* error_message);
+libtextclassifier3::Status AbortedError(const char* error_message);
+libtextclassifier3::Status OutOfRangeError(const char* error_message);
+libtextclassifier3::Status UnimplementedError(const char* error_message);
+libtextclassifier3::Status InternalError(const char* error_message);
+libtextclassifier3::Status UnavailableError(const char* error_message);
+libtextclassifier3::Status DataLossError(const char* error_message);
+libtextclassifier3::Status UnauthenticatedError(const char* error_message);
+
 libtextclassifier3::Status CancelledError(std::string error_message);
 libtextclassifier3::Status UnknownError(std::string error_message);
 libtextclassifier3::Status InvalidArgumentError(std::string error_message);
diff --git a/icing/absl_ports/mutex.h b/icing/absl_ports/mutex.h
index 382a100..aacebd5 100644
--- a/icing/absl_ports/mutex.h
+++ b/icing/absl_ports/mutex.h
@@ -52,6 +52,9 @@
       : lock_(*mu) {}
   ~unique_lock() ICING_UNLOCK_FUNCTION() = default;
 
+  // For fine-grained locking if necessary.
+  void unlock() ICING_UNLOCK_FUNCTION() { lock_.unlock(); }
+  void lock() ICING_EXCLUSIVE_LOCK_FUNCTION() { lock_.lock(); }
  private:
   std::unique_lock<shared_mutex> lock_;
 };
diff --git a/icing/feature-flags.h b/icing/feature-flags.h
index 0231fe5..1c3c977 100644
--- a/icing/feature-flags.h
+++ b/icing/feature-flags.h
@@ -15,6 +15,8 @@
 #ifndef ICING_FEATURE_FLAGS_H_
 #define ICING_FEATURE_FLAGS_H_
 
+#include <cstdint>
+
 namespace icing {
 namespace lib {
 
@@ -26,7 +28,21 @@
                         bool enable_repeated_field_joins,
                         bool enable_embedding_backup_generation,
                         bool enable_schema_database,
-                        bool release_backup_schema_file_if_overlay_present)
+                        bool release_backup_schema_file_if_overlay_present,
+                        bool enable_strict_page_byte_size_limit,
+                        bool enable_smaller_decompression_buffer_size,
+                        bool enable_eigen_embedding_scoring,
+                        bool enable_passing_filter_to_children,
+                        bool enable_proto_log_new_header_format,
+                        bool enable_embedding_iterator_v2,
+                        bool enable_reusable_decompression_buffer,
+                        bool enable_schema_type_id_optimization,
+                        bool enable_optimize_improvements,
+                        int64_t expired_document_purge_threshold_ms,
+                        bool enable_non_existent_qualified_id_join,
+                        bool enable_skip_set_schema_type_equality_check,
+                        bool enable_embed_query_optimization,
+                        bool enable_schema_definition_deduping)
       : allow_circular_schema_definitions_(allow_circular_schema_definitions),
         enable_scorable_properties_(enable_scorable_properties),
         enable_embedding_quantization_(enable_embedding_quantization),
@@ -34,7 +50,26 @@
         enable_embedding_backup_generation_(enable_embedding_backup_generation),
         enable_schema_database_(enable_schema_database),
         release_backup_schema_file_if_overlay_present_(
-            release_backup_schema_file_if_overlay_present) {}
+            release_backup_schema_file_if_overlay_present),
+        enable_strict_page_byte_size_limit_(enable_strict_page_byte_size_limit),
+        enable_smaller_decompression_buffer_size_(
+            enable_smaller_decompression_buffer_size),
+        enable_eigen_embedding_scoring_(enable_eigen_embedding_scoring),
+        enable_passing_filter_to_children_(enable_passing_filter_to_children),
+        enable_proto_log_new_header_format_(enable_proto_log_new_header_format),
+        enable_embedding_iterator_v2_(enable_embedding_iterator_v2),
+        enable_reusable_decompression_buffer_(
+            enable_reusable_decompression_buffer),
+        enable_schema_type_id_optimization_(enable_schema_type_id_optimization),
+        enable_optimize_improvements_(enable_optimize_improvements),
+        expired_document_purge_threshold_ms_(
+            expired_document_purge_threshold_ms),
+        enable_non_existent_qualified_id_join_(
+            enable_non_existent_qualified_id_join),
+        enable_skip_set_schema_type_equality_check_(
+            enable_skip_set_schema_type_equality_check),
+        enable_embed_query_optimization_(enable_embed_query_optimization),
+        enable_schema_definition_deduping_(enable_schema_definition_deduping) {}
 
   bool allow_circular_schema_definitions() const {
     return allow_circular_schema_definitions_;
@@ -62,6 +97,62 @@
     return release_backup_schema_file_if_overlay_present_;
   }
 
+  bool enable_strict_page_byte_size_limit() const {
+    return enable_strict_page_byte_size_limit_;
+  }
+
+  bool enable_smaller_decompression_buffer_size() const {
+    return enable_smaller_decompression_buffer_size_;
+  }
+
+  bool enable_eigen_embedding_scoring() const {
+    return enable_eigen_embedding_scoring_;
+  }
+
+  bool enable_passing_filter_to_children() const {
+    return enable_passing_filter_to_children_;
+  }
+
+  bool enable_proto_log_new_header_format() const {
+    return enable_proto_log_new_header_format_;
+  }
+
+  bool enable_embedding_iterator_v2() const {
+    return enable_embedding_iterator_v2_;
+  }
+
+  bool enable_reusable_decompression_buffer() const {
+    return enable_reusable_decompression_buffer_;
+  }
+
+  bool enable_schema_type_id_optimization() const {
+    return enable_schema_type_id_optimization_;
+  }
+
+  bool enable_optimize_improvements() const {
+    return enable_optimize_improvements_;
+  }
+
+  int64_t expired_document_purge_threshold_ms() const {
+    return expired_document_purge_threshold_ms_;
+  }
+
+  bool enable_non_existent_qualified_id_join() const {
+    return enable_non_existent_qualified_id_join_;
+  }
+
+  bool enable_skip_set_schema_type_equality_check() const {
+    return enable_skip_set_schema_type_equality_check_;
+  }
+
+  bool enable_embed_query_optimization() const {
+    return enable_embed_query_optimization_;
+  }
+
+  bool enable_schema_definition_deduping() const {
+    return enable_schema_definition_deduping_;
+  }
+
  private:
   // Whether to allow circular references in the schema definition. This was
   // added in the Android U timeline and is not a trunk-stable flag.
@@ -83,6 +174,79 @@
   bool enable_schema_database_;
 
   bool release_backup_schema_file_if_overlay_present_;
+
+  // Whether to enable strict page byte size limit enforcement in
+  // ResultRetrieverV2.
+  bool enable_strict_page_byte_size_limit_;
+
+  bool enable_smaller_decompression_buffer_size_;
+
+  // Whether to enable the Eigen library for embedding scoring.
+  // If set to true **and** Eigen is compiled in (when ICING_DISABLE_EIGEN is
+  // not defined), Eigen will be used for embedding scoring.
+  bool enable_eigen_embedding_scoring_;
+
+  bool enable_passing_filter_to_children_;
+
+  // Whether to enable the new header format (refactor legacy format and
+  // introduce unsynced tail checksum) related changes in
+  // PortableFileBackedProtoLog.
+  bool enable_proto_log_new_header_format_;
+
+  bool enable_embedding_iterator_v2_;
+
+  // Whether PortableFileBackedProtoLog should retain a decompression buffer
+  // that reads can reuse rather than allocating a new one for each read.
+  bool enable_reusable_decompression_buffer_;
+
+  bool enable_schema_type_id_optimization_;
+
+  // Whether to enable a few minor improvements to Optimize:
+  // 1. Avoid unnecessary Status allocs for deleted/expired docs
+  // 2. Remove an unnecessary persist to disk call
+  bool enable_optimize_improvements_;
+
+  // The time threshold for an expired document to be purged.
+  // - Since we schedule a task to purge expired documents according to the next
+  //   expiration time of the documents, it is possible that some documents
+  //   expire within a small time window and the task executes too frequently.
+  // - Therefore, we use this flag to purge more documents that also expire in a
+  //   short period of time after the current time.
+  //
+  // For example, if the value is 1000 ms and the current time is 10000 ms:
+  // - All documents that are expired before 10000 ms will be purged, since they
+  //   are already expired.
+  // - Additionally, we will also purge documents that expire in the next 1000
+  //   ms, i.e. (10000, 11000] ms.
+  int64_t expired_document_purge_threshold_ms_;
+
+  // Whether to allow a document to reference a join parent (by its qualified
+  // id) that does not yet exist. When enabled, the join index will handle cases
+  // where a child document is indexed before its parent. The join relationship
+  // will be established once the parent document is indexed. If disabled, the
+  // join relationship will be lost if the child is indexed before the parent.
+  bool enable_non_existent_qualified_id_join_;
+
+  // Whether to skip the schema type equality check during SetSchema. We
+  // serialize the schema proto to strings when doing this check, which is slow.
+  //
+  // This will be set to true in AppSearch.
+  // - AppSearch already checks if schema types are unchanged for a new
+  //   SetSchema request, and skips the Icing interaction entirely if that is
+  //   the case.
+  // - Therefore, the Icing-side equality check in SetSchema is redundant and
+  //   can be skipped if caller is AppSearch.
+  bool enable_skip_set_schema_type_equality_check_;
+
+  // Whether to enable a query optimization that will rewrite embedding query
+  // iterators that are being AND'ed with other iterators such that those other
+  // iterators can be pushed down into the embedding iterator as a delegate.
+  // This allows us to avoid reading and scoring embeddings for documents that
+  // don't match the other requirements of the query.
+  bool enable_embed_query_optimization_;
+
+  // Whether to enable deduping for the schema's type definitions.
+  bool enable_schema_definition_deduping_;
 };
 
 }  // namespace lib
diff --git a/icing/file/file-backed-proto-log.h b/icing/file/file-backed-proto-log.h
index 0c06f77..985f50d 100644
--- a/icing/file/file-backed-proto-log.h
+++ b/icing/file/file-backed-proto-log.h
@@ -573,7 +573,11 @@
   // Deserialize proto
   ProtoT proto;
   if (header_->compress) {
-    protobuf_ports::GzipInputStream decompress_stream(&proto_stream);
+    std::unique_ptr<uint8_t[]> buffer =
+        std::make_unique<uint8_t[]>(protobuf_ports::kDefaultBufferSize);
+    protobuf_ports::GzipInputStream decompress_stream(
+        &proto_stream, protobuf_ports::GzipInputStream::AUTO, buffer.get(),
+        protobuf_ports::kDefaultBufferSize);
     proto.ParseFromZeroCopyStream(&decompress_stream);
   } else {
     proto.ParseFromZeroCopyStream(&proto_stream);
diff --git a/icing/file/file-backed-proto.h b/icing/file/file-backed-proto.h
index 65566c6..3504f96 100644
--- a/icing/file/file-backed-proto.h
+++ b/icing/file/file-backed-proto.h
@@ -196,20 +196,24 @@
                 << " of size: " << file_size;
 
   Header header;
-  if (!filesystem_->PRead(fd.get(), &header, sizeof(Header), /*offset=*/0)) {
+  if (filesystem_->PRead(fd.get(), &header, sizeof(Header), /*offset=*/0) !=
+      sizeof(Header)) {
     return absl_ports::InternalError(
         absl_ports::StrCat("Unable to read header of: ", file_path_));
   }
 
   if (header.magic != Header::kMagic) {
-    return absl_ports::InternalError(
-        absl_ports::StrCat("Invalid header kMagic for: ", file_path_));
+    ICING_LOG(ERROR) << "Invalid header magic for FileBackedProto "
+                     << file_path_ << ". Expected: " << Header::kMagic
+                     << ", actual: " << header.magic;
+    return absl_ports::InternalError(absl_ports::StrCat(
+        "Invalid header magic for FileBackedProto: ", file_path_));
   }
 
   int proto_size = file_size - sizeof(Header);
   auto buffer = std::make_unique<uint8_t[]>(proto_size);
-  if (!filesystem_->PRead(fd.get(), buffer.get(), proto_size,
-                          /*offset=*/sizeof(Header))) {
+  if (filesystem_->PRead(fd.get(), buffer.get(), proto_size,
+                         /*offset=*/sizeof(Header)) != proto_size) {
     return absl_ports::InternalError(
         absl_ports::StrCat("File read failed: ", file_path_));
   }
diff --git a/icing/file/file-backed-vector.h b/icing/file/file-backed-vector.h
index 8bd7161..04ee7c3 100644
--- a/icing/file/file-backed-vector.h
+++ b/icing/file/file-backed-vector.h
@@ -701,9 +701,12 @@
   // Make sure the header is still valid before we use any of its values. This
   // should technically be included in the header_checksum check below, but this
   // is a quick/fast check that can save us from an extra crc computation.
-  if (header->kMagic != FileBackedVector<T>::Header::kMagic) {
-    return absl_ports::InternalError(
-        absl_ports::StrCat("Invalid header kMagic for ", file_path));
+  if (header->magic != Header::kMagic) {
+    ICING_LOG(ERROR) << "Invalid header magic for FileBackedVector "
+                     << file_path << ". Expected: " << Header::kMagic
+                     << ", actual: " << header->magic;
+    return absl_ports::InternalError(absl_ports::StrCat(
+        "Invalid header magic for FileBackedVector: ", file_path));
   }
 
   // Check header
diff --git a/icing/file/file-backed-vector_test.cc b/icing/file/file-backed-vector_test.cc
index 4561e39..c7fbedc 100644
--- a/icing/file/file-backed-vector_test.cc
+++ b/icing/file/file-backed-vector_test.cc
@@ -1364,7 +1364,7 @@
   // to corruption of the header.
   FileBackedVector<char>::Header header;
   ASSERT_THAT(filesystem_.PRead(fd_, &header, sizeof(header), /*offset=*/0),
-              IsTrue());
+              Eq(sizeof(header)));
   header.num_elements = 1;
   ASSERT_THAT(filesystem_.PWrite(fd_, /*offset=*/0, &header, sizeof(header)),
               IsTrue());
@@ -1396,7 +1396,7 @@
   // of the underlying file.
   FileBackedVector<char>::Header header;
   ASSERT_THAT(filesystem_.PRead(fd_, &header, sizeof(header), /*offset=*/0),
-              IsTrue());
+              Eq(sizeof(header)));
   int64_t file_size = filesystem_.GetFileSize(fd_);
   int64_t allocated_elements_size = file_size - sizeof(header);
   header.num_elements = (allocated_elements_size / sizeof(char)) + 1;
diff --git a/icing/file/filesystem.cc b/icing/file/filesystem.cc
index cd905e7..22d1984 100644
--- a/icing/file/filesystem.cc
+++ b/icing/file/filesystem.cc
@@ -26,11 +26,14 @@
 
 #include <algorithm>
 #include <cerrno>
+#include <cstddef>
 #include <cstdint>
+#include <cstring>
+#include <memory>
 #include <unordered_set>
+#include <vector>
 
 #include "icing/absl_ports/str_cat.h"
-#include "icing/legacy/core/icing-string-util.h"
 #include "icing/util/logging.h"
 
 using std::vector;
@@ -125,8 +128,14 @@
                            const std::unordered_set<std::string>& exclude,
                            bool recursive, const char* prefix,
                            std::vector<std::string>* entries) {
-  DIR* dir = opendir(dir_name);
-  if (!dir) {
+  auto closer = [](DIR* dir) {
+    if (closedir(dir) != 0) {
+      ICING_LOG(ERROR) << "Error closing dir (" << errno << ") "
+                       << strerror(errno);
+    }
+  };
+  std::unique_ptr<DIR, decltype(closer)> dir(opendir(dir_name), closer);
+  if (dir == nullptr) {
     LogOpenError("Unable to open directory ", dir_name, ": ", errno);
     return false;
   }
@@ -136,7 +145,7 @@
   // may be statically allocated, so don't free it.
   dirent* p;
   // readdir's implementation seems to be thread safe.
-  while ((p = readdir(dir)) != nullptr) {
+  while ((p = readdir(dir.get())) != nullptr) {
     std::string file_name(p->d_name);
     if (file_name == "." || file_name == ".." ||
         exclude.find(file_name) != exclude.end()) {
@@ -155,9 +164,6 @@
       }
     }
   }
-  if (closedir(dir) != 0) {
-    ICING_LOG(ERROR) << "Error closing " << dir_name << " " << strerror(errno);
-  }
   return true;
 }
 
@@ -182,7 +188,8 @@
   ICING_VLOG(1) << "Deleting file " << file_name;
   int ret = unlink(file_name);
   if (ret != 0 && errno != ENOENT) {
-    ICING_LOG(ERROR) << "Deleting file " << file_name << " failed: " << strerror(errno);
+    ICING_LOG(ERROR) << "Deleting file " << file_name << " failed: (" << errno
+                     << ") " << strerror(errno);
     return false;
   }
   return true;
@@ -191,7 +198,8 @@
 bool Filesystem::DeleteDirectory(const char* dir_name) const {
   int ret = rmdir(dir_name);
   if (ret != 0 && errno != ENOENT) {
-    ICING_LOG(ERROR) << "Deleting directory " << dir_name << " failed: " << strerror(errno);
+    ICING_LOG(ERROR) << "Deleting directory " << dir_name << " failed: ("
+                     << errno << ") " << strerror(errno);
     return false;
   }
   return true;
@@ -204,7 +212,8 @@
     if (errno == ENOENT) {
       return true;  // If directory didn't exist, this was successful.
     }
-    ICING_LOG(ERROR) << "Stat " << dir_name << " failed: " << strerror(errno);
+    ICING_LOG(ERROR) << "Stat " << dir_name << " failed: (" << errno << ") "
+                     << strerror(errno);
     return false;
   }
   vector<std::string> entries;
@@ -217,7 +226,8 @@
        ++i) {
     std::string filename = std::string(dir_name) + '/' + *i;
     if (stat(filename.c_str(), &st) < 0) {
-      ICING_LOG(ERROR) << "Stat " << filename << " failed: " << strerror(errno);
+      ICING_LOG(ERROR) << "Stat " << filename << " failed: (" << errno << ") "
+                       << strerror(errno);
       success = false;
     } else if (S_ISDIR(st.st_mode)) {
       success = DeleteDirectoryRecursively(filename.c_str()) && success;
@@ -240,7 +250,8 @@
     exists = S_ISREG(st.st_mode) != 0;
   } else {
     if (errno != ENOENT) {
-      ICING_LOG(ERROR) << "Unable to stat file " << file_name << ": " << strerror(errno);
+      ICING_LOG(ERROR) << "Unable to stat file " << file_name << ": (" << errno
+                       << ") " << strerror(errno);
     }
     exists = false;
   }
@@ -254,7 +265,8 @@
     exists = S_ISDIR(st.st_mode) != 0;
   } else {
     if (errno != ENOENT) {
-      ICING_LOG(ERROR) << "Unable to stat directory " << dir_name << ": " << strerror(errno);
+      ICING_LOG(ERROR) << "Unable to stat directory " << dir_name << ": ("
+                       << errno << ") " << strerror(errno);
     }
     exists = false;
   }
@@ -365,11 +377,8 @@
 int64_t Filesystem::GetFileSize(int fd) const {
   struct stat st;
   if (fstat(fd, &st) < 0) {
-    if (errno == ENOENT) {
-      ICING_VLOG(1) << "Unable to stat file: " << strerror(errno);
-    } else {
-      ICING_LOG(WARNING) << "Unable to stat file: " << strerror(errno);
-    }
+      ICING_LOG(WARNING) << "Unable to stat file: (" << errno << ") "
+                         << strerror(errno);
     return kBadFileSize;
   }
   return st.st_size;
@@ -379,9 +388,11 @@
   struct stat st;
   if (stat(filename, &st) < 0) {
     if (errno == ENOENT) {
-      ICING_VLOG(1) << "Unable to stat file " << filename << ": " << strerror(errno);
+      ICING_VLOG(1) << "Unable to stat file " << filename << ": "
+          << strerror(errno);
     } else {
-      ICING_LOG(WARNING) << "Unable to stat file " << filename << ": " << strerror(errno);
+      ICING_LOG(WARNING) << "Unable to stat file " << filename << ": (" << errno
+                         << ") " << strerror(errno);
     }
     return kBadFileSize;
   }
@@ -390,7 +401,8 @@
 
 bool Filesystem::Truncate(int fd, int64_t new_size) const {
   if (ftruncate(fd, new_size) != 0) {
-    ICING_LOG(ERROR) << "Unable to truncate file: " << strerror(errno);
+    ICING_LOG(ERROR) << "Unable to truncate file: (" << errno << ") "
+                     << strerror(errno);
     return false;
   }
   lseek(fd, new_size, SEEK_SET);
@@ -409,7 +421,8 @@
 
 bool Filesystem::Grow(int fd, int64_t new_size) const {
   if (ftruncate(fd, new_size) != 0) {
-    ICING_LOG(ERROR) << "Unable to grow file: " << strerror(errno);
+    ICING_LOG(ERROR) << "Unable to grow file: (" << errno << ") "
+                     << strerror(errno);
     return false;
   }
 
@@ -431,10 +444,14 @@
   size_t write_len = data_size;
   do {
     // Don't try to write too much at once.
-    size_t chunk_size = std::min<size_t>(write_len, 64u * 1024);
-    ssize_t wrote = write(fd, data, chunk_size);
+#ifdef __APPLE__
+    // TEMP_FAILURE_RETRY is not defined in unistd.h on iOS.
+    ssize_t wrote = write(fd, data, write_len);
+#else  // __APPLE__
+    ssize_t wrote = TEMP_FAILURE_RETRY(write(fd, data, write_len));
+#endif // __APPLE__
     if (wrote < 0) {
-      ICING_LOG(ERROR) << "Bad write: " << strerror(errno);
+      ICING_LOG(ERROR) << "Bad write: (" << errno << ") " << strerror(errno);
       return false;
     }
     data = static_cast<const uint8_t*>(data) + wrote;
@@ -469,7 +486,7 @@
   }
   uint64_t size = GetFileSize(*src_fd);
   std::unique_ptr<uint8_t[]> buf = std::make_unique<uint8_t[]>(size);
-  if (!Read(*src_fd, buf.get(), size)) {
+  if (Read(*src_fd, buf.get(), size) != size) {
     return false;
   }
   return Write(*dst_fd, buf.get(), size);
@@ -477,15 +494,21 @@
 
 bool Filesystem::CopyDirectory(const char* src_dir, const char* dst_dir,
                                bool recursive) const {
-  DIR* dir = opendir(src_dir);
-  if (!dir) {
+  auto closer = [](DIR* dir) {
+    if (closedir(dir) != 0) {
+      ICING_LOG(ERROR) << "Error closing dir (" << errno << ") "
+                       << strerror(errno);
+    }
+  };
+  std::unique_ptr<DIR, decltype(closer)> dir(opendir(src_dir), closer);
+  if (dir == nullptr) {
     LogOpenError("Unable to open directory ", src_dir, ": ", errno);
     return false;
   }
 
   dirent* p;
   // readdir's implementation seems to be thread safe.
-  while ((p = readdir(dir)) != nullptr) {
+  while ((p = readdir(dir.get())) != nullptr) {
     std::string file_name(p->d_name);
     if (file_name == "." || file_name == "..") {
       continue;
@@ -511,9 +534,6 @@
       }
     }
   }
-  if (closedir(dir) != 0) {
-    ICING_LOG(ERROR) << "Error closing " << src_dir << ": " << strerror(errno);
-  }
   return true;
 }
 
@@ -521,11 +541,14 @@
                         size_t data_size) const {
   size_t write_len = data_size;
   do {
-    // Don't try to write too much at once.
-    size_t chunk_size = std::min<size_t>(write_len, 64u * 1024);
-    ssize_t wrote = pwrite(fd, data, chunk_size, offset);
+#ifdef __APPLE__
+    // TEMP_FAILURE_RETRY is not defined in unistd.h on iOS.
+    ssize_t wrote = pwrite(fd, data, write_len, offset);
+#else  // __APPLE__
+    ssize_t wrote = TEMP_FAILURE_RETRY(pwrite(fd, data, write_len, offset));
+#endif // __APPLE__
     if (wrote < 0) {
-      ICING_LOG(ERROR) << "Bad write: " << strerror(errno);
+      ICING_LOG(ERROR) << "Bad write: (" << errno << ") " << strerror(errno);
       return false;
     }
     data = static_cast<const uint8_t*>(data) + wrote;
@@ -547,45 +570,78 @@
   return success;
 }
 
-bool Filesystem::Read(int fd, void* buf, size_t buf_size) const {
-  ssize_t read_status = read(fd, buf, buf_size);
-  if (read_status < 0) {
-    ICING_LOG(ERROR) << "Bad read: " << strerror(errno);
-    return false;
+ssize_t Filesystem::Read(int fd, void* buf, size_t buf_size) const {
+  // convenience for pointer arithmetic below.
+  char* buf_ptr = static_cast<char*>(buf);
+  ssize_t processed_size = 0;
+  while (processed_size < buf_size) {
+    ssize_t read_status =
+        read(fd, buf_ptr + processed_size, buf_size - processed_size);
+    if (read_status < 0) {
+      ICING_LOG(ERROR) << "Bad read: (" << errno << ") " << strerror(errno);
+      return read_status;
+    }
+    if (read_status < buf_size - processed_size) {
+      ICING_LOG(ERROR) << "Read less than requested: " << read_status << " < "
+                       << buf_size - processed_size;
+    }
+    if (read_status == 0) {
+      // EOF. Finish reading.
+      return processed_size;
+    }
+    processed_size += read_status;
   }
-  return true;
+  return processed_size;
 }
 
-bool Filesystem::Read(const char* filename, void* buf, size_t buf_size) const {
+ssize_t Filesystem::Read(const char* filename, void* buf,
+                         size_t buf_size) const {
   int fd = OpenForRead(filename);
   if (fd == -1) {
-    return false;
+    return -1;
   }
 
-  bool success = Read(fd, buf, buf_size);
+  ssize_t bytes_read = Read(fd, buf, buf_size);
   close(fd);
-  return success;
+  return bytes_read;
 }
 
-bool Filesystem::PRead(int fd, void* buf, size_t buf_size, off_t offset) const {
-  ssize_t read_status = pread(fd, buf, buf_size, offset);
-  if (read_status < 0) {
-    ICING_LOG(ERROR) << "Bad read: " << strerror(errno);
-    return false;
+ssize_t Filesystem::PRead(int fd, void* buf, size_t buf_size,
+                          off_t offset) const {
+  // convenience for pointer arithmetic below.
+  char* buf_ptr = static_cast<char*>(buf);
+  size_t processed_size = 0;
+  while (processed_size < buf_size) {
+    ssize_t read_status =
+        pread(fd, buf_ptr + processed_size, buf_size - processed_size,
+              offset + processed_size);
+    if (read_status < 0) {
+      ICING_LOG(ERROR) << "Bad read: (" << errno << ") " << strerror(errno);
+      return read_status;
+    }
+    if (read_status < buf_size - processed_size) {
+      ICING_LOG(ERROR) << "Read less than requested: " << read_status << " < "
+                       << buf_size - processed_size;
+    }
+    if (read_status == 0) {
+      // EOF. Finish reading.
+      return processed_size;
+    }
+    processed_size += read_status;
   }
-  return true;
+  return processed_size;
 }
 
-bool Filesystem::PRead(const char* filename, void* buf, size_t buf_size,
-                       off_t offset) const {
+ssize_t Filesystem::PRead(const char* filename, void* buf, size_t buf_size,
+                          off_t offset) const {
   int fd = OpenForRead(filename);
   if (fd == -1) {
-    return false;
+    return -1;
   }
 
-  bool success = PRead(fd, buf, buf_size, offset);
+  ssize_t bytes_read = PRead(fd, buf, buf_size, offset);
   close(fd);
-  return success;
+  return bytes_read;
 }
 
 bool Filesystem::DataSync(int fd) const {
@@ -596,7 +652,8 @@
 #endif
 
   if (result < 0) {
-    ICING_LOG(ERROR) << "Unable to sync data: " << strerror(errno);
+    ICING_LOG(ERROR) << "Unable to sync data: (" << errno << ") "
+                     << strerror(errno);
     return false;
   }
   return true;
@@ -604,7 +661,8 @@
 
 bool Filesystem::RenameFile(const char* old_name, const char* new_name) const {
   if (rename(old_name, new_name) < 0) {
-    ICING_LOG(ERROR) << "Unable to rename file " << old_name << " to " << new_name << ": " << strerror(errno);
+    ICING_LOG(ERROR) << "Unable to rename file " << old_name << " to "
+                     << new_name << ": (" << errno << ") " << strerror(errno);
     return false;
   }
   return true;
@@ -642,7 +700,8 @@
     if (mkdir(dir_name, S_IRUSR | S_IWUSR | S_IXUSR) == 0) {
       success = true;
     } else {
-      ICING_LOG(ERROR) << "Creating directory " << dir_name << " failed: " << strerror(errno);
+      ICING_LOG(ERROR) << "Creating directory " << dir_name << " failed: ("
+                       << errno << ") " << strerror(errno);
     }
   }
   return success;
@@ -662,7 +721,8 @@
 int64_t Filesystem::GetDiskUsage(int fd) const {
   struct stat st;
   if (fstat(fd, &st) < 0) {
-    ICING_LOG(ERROR) << "Unable to stat file: " << strerror(errno);
+    ICING_LOG(ERROR) << "Unable to stat file: (" << errno << ") "
+                     << strerror(errno);
     return kBadFileSize;
   }
   return st.st_blocks * kStatBlockSize;
@@ -671,7 +731,8 @@
 int64_t Filesystem::GetFileDiskUsage(const char* path) const {
   struct stat st;
   if (stat(path, &st) != 0) {
-    ICING_LOG(ERROR) << "Unable to stat " << path << ": " << strerror(errno);
+    ICING_LOG(ERROR) << "Unable to stat " << path << ": (" << errno << ") "
+                     << strerror(errno);
     return kBadFileSize;
   }
   return st.st_blocks * kStatBlockSize;
@@ -680,7 +741,8 @@
 int64_t Filesystem::GetDiskUsage(const char* path) const {
   struct stat st;
   if (stat(path, &st) != 0) {
-    ICING_LOG(ERROR) << "Unable to stat " << path << ": " << strerror(errno);
+    ICING_LOG(ERROR) << "Unable to stat " << path << ": (" << errno << ") "
+                     << strerror(errno);
     return kBadFileSize;
   }
   int64_t result = st.st_blocks * kStatBlockSize;
diff --git a/icing/file/filesystem.h b/icing/file/filesystem.h
index dd2c5d1..71eba0b 100644
--- a/icing/file/filesystem.h
+++ b/icing/file/filesystem.h
@@ -183,11 +183,11 @@
   // Reads from a file. Returns true if data was successfully read out. If the
   // file is seekable, read starts at the file offset, and the file offset is
   // incremented by number of bytes read.
-  virtual bool Read(int fd, void* buf, size_t buf_size) const;
-  virtual bool Read(const char* filename, void* buf, size_t buf_size) const;
-  virtual bool PRead(int fd, void* buf, size_t buf_size, off_t offset) const;
-  virtual bool PRead(const char* filename, void* buf, size_t buf_size,
-                     off_t offset) const;
+  virtual ssize_t Read(int fd, void* buf, size_t buf_size) const;
+  virtual ssize_t Read(const char* filename, void* buf, size_t buf_size) const;
+  virtual ssize_t PRead(int fd, void* buf, size_t buf_size, off_t offset) const;
+  virtual ssize_t PRead(const char* filename, void* buf, size_t buf_size,
+                        off_t offset) const;
 
   // Syncs the file to disk (fdatasync). Returns true on success.
   virtual bool DataSync(int fd) const;
diff --git a/icing/file/filesystem_test.cc b/icing/file/filesystem_test.cc
index 214180e..378ef98 100644
--- a/icing/file/filesystem_test.cc
+++ b/icing/file/filesystem_test.cc
@@ -17,8 +17,11 @@
 #include "icing/file/filesystem.h"
 
 #include <algorithm>
+#include <chrono>
 #include <cstdint>
+#include <cstring>
 #include <string>
+#include <thread>
 #include <unordered_set>
 #include <vector>
 
@@ -419,38 +422,89 @@
   EXPECT_TRUE(filesystem.Write(fd, data.c_str(), strlen(data.c_str())));
 
   std::string hello;
-  hello.resize(strlen("hello"));
-  EXPECT_TRUE(filesystem.Read(foo_file.c_str(), &hello[0], strlen("hello")));
+  size_t hello_len = strlen("hello");
+  hello.resize(hello_len);
+  EXPECT_THAT(filesystem.Read(foo_file.c_str(), &hello[0], hello_len),
+              Eq(hello_len));
   EXPECT_THAT(hello, Eq("hello"));
 
   // Read starts from wherever file offset is at the moment.
   filesystem.SetPosition(fd, 0);
   hello.clear();
-  hello.resize(strlen("hello"));
-  EXPECT_TRUE(filesystem.Read(fd, &hello[0], strlen("hello")));
+  hello.resize(hello_len);
+  EXPECT_THAT(filesystem.Read(fd, &hello[0], hello_len), Eq(hello_len));
   EXPECT_THAT(hello, Eq("hello"));
 
   // Shouldn't need to move file offset anymore since file offset gets updated
   // after the read.
   std::string world;
-  world.resize(strlen(" world"));
-  EXPECT_TRUE(filesystem.Read(fd, &world[0], strlen(" world")));
+  size_t world_len = strlen(" world");
+  world.resize(world_len);
+  EXPECT_THAT(filesystem.Read(fd, &world[0], world_len), Eq(world_len));
   EXPECT_THAT(world, Eq(" world"));
 
   // PRead should not be dependent on the file offset
   world.clear();
-  world.resize(strlen(" world"));
-  EXPECT_TRUE(
-      filesystem.PRead(fd, &world[0], strlen(" world"), strlen("hello")));
+  world.resize(world_len);
+  EXPECT_THAT(filesystem.PRead(fd, &world[0], world_len, hello_len),
+              Eq(world_len));
   EXPECT_THAT(world, Eq(" world"));
 
   hello.clear();
-  hello.resize(strlen("hello"));
-  EXPECT_TRUE(
-      filesystem.PRead(foo_file.c_str(), &hello[0], strlen("hello"), 0));
+  hello.resize(hello_len);
+  EXPECT_THAT(filesystem.PRead(foo_file.c_str(), &hello[0], hello_len, 0),
+              Eq(hello_len));
   EXPECT_THAT(hello, Eq("hello"));
 }
 
+TEST_F(FilesystemTest, ReadInChunks) {
+  int pipe_fd[2];
+  ASSERT_THAT(pipe(pipe_fd), Eq(0));
+  ScopedFd read_fd(pipe_fd[0]);
+  ScopedFd write_fd(pipe_fd[1]);
+
+  std::string read_data(200, 'a');
+  bool read_success = false;
+  Filesystem filesystem;
+  auto read_callable = [&]() {
+    read_success = filesystem.Read(read_fd.get(), &read_data[0],
+                                   read_data.size()) == read_data.size();
+  };
+
+  std::string write_chunks(50, 'b');
+  auto write_callable = [&]() {
+    for (int i = 0; i < 4; ++i) {
+      filesystem.Write(write_fd.get(), &write_chunks[0], write_chunks.size());
+      std::this_thread::sleep_for(std::chrono::milliseconds(1));
+    }
+  };
+
+  std::thread read_thread(read_callable);
+  std::thread write_thread(write_callable);
+  read_thread.join();
+  write_thread.join();
+  EXPECT_TRUE(read_success);
+  std::string expected_data(200, 'b');
+  EXPECT_THAT(read_data, Eq(expected_data));
+}
+
+TEST_F(FilesystemTest, ReadFileSmallerThanBufferSize) {
+  Filesystem filesystem;
+  const std::string foo_file = temp_dir_ + "/foo_file";
+  ScopedFd fd(filesystem.OpenForWrite(foo_file.c_str()));;
+  ASSERT_TRUE(fd.is_valid());
+
+  std::string write_buf(100, 'b');
+  ASSERT_TRUE(filesystem.Write(fd.get(), &write_buf[0], write_buf.length()));
+
+  std::string read_buf(200, 'a');
+  EXPECT_THAT(filesystem.PRead(fd.get(), &read_buf[0], read_buf.length(), 0),
+              Eq(write_buf.length()));
+
+  std::string expected_read_buf = std::string(100, 'b') + std::string(100, 'a');
+  EXPECT_THAT(read_buf, Eq(expected_read_buf));
+}
+
 TEST_F(FilesystemTest, CopyDirectory) {
   Filesystem filesystem;
 
diff --git a/icing/file/marker-file_test.cc b/icing/file/marker-file_test.cc
index cd7d655..ba51769 100644
--- a/icing/file/marker-file_test.cc
+++ b/icing/file/marker-file_test.cc
@@ -123,8 +123,7 @@
   EXPECT_THAT(filesystem_.FileExists(file_path.c_str()), IsTrue());
   EXPECT_THAT(filesystem_.GetFileSize(file_path.c_str()), Eq(4));
   char buf[4];
-  EXPECT_THAT(filesystem_.Read(file_path.c_str(), buf, /*buf_size=*/4),
-              IsTrue());
+  EXPECT_THAT(filesystem_.Read(file_path.c_str(), buf, /*buf_size=*/4), Eq(4));
   EXPECT_THAT(std::string(buf, 4), Eq("test"));
 }
 
diff --git a/icing/file/memory-mapped-file-backed-proto-log.h b/icing/file/memory-mapped-file-backed-proto-log.h
index cc8f35f..ab0ddcc 100644
--- a/icing/file/memory-mapped-file-backed-proto-log.h
+++ b/icing/file/memory-mapped-file-backed-proto-log.h
@@ -31,6 +31,7 @@
 #include "icing/file/memory-mapped-file.h"
 #include "icing/legacy/core/icing-string-util.h"
 #include "icing/util/crc32.h"
+#include "icing/util/logging.h"
 #include "icing/util/status-macros.h"
 
 namespace icing {
@@ -159,6 +160,9 @@
     ProtoMetadata proto_metadata) {
   uint8_t magic_number = proto_metadata >> 24;
   if (magic_number != kProtoMagic) {
+    ICING_LOG(ERROR)
+        << "Invalid header magic for MemoryMappedFileBackedProtoLog. Expected: "
+        << kProtoMagic << ", actual: " << magic_number;
     return absl_ports::InvalidArgumentError(
         "Proto metadata has invalid magic number");
   }
diff --git a/icing/file/memory-mapped-file_test.cc b/icing/file/memory-mapped-file_test.cc
index 16f76e6..8c3891d 100644
--- a/icing/file/memory-mapped-file_test.cc
+++ b/icing/file/memory-mapped-file_test.cc
@@ -161,8 +161,9 @@
     ASSERT_TRUE(sfd.is_valid());
     int buf_size = 10;
     auto buf = std::make_unique<char[]>(buf_size);
-    ASSERT_TRUE(filesystem_.PRead(sfd.get(), buf.get(), buf_size,
-                                  pre_mapping_file_offset));
+    ASSERT_THAT(filesystem_.PRead(sfd.get(), buf.get(), buf_size,
+                                  pre_mapping_file_offset),
+                Eq(buf_size));
     EXPECT_THAT(buf.get()[0], Eq('a'));
   }
 }
@@ -300,8 +301,9 @@
   ASSERT_TRUE(sfd.is_valid());
   int buf_size = 1;
   auto buf = std::make_unique<char[]>(buf_size);
-  ASSERT_TRUE(filesystem_.PRead(sfd.get(), buf.get(), buf_size,
-                                pre_mapping_file_offset));
+  ASSERT_THAT(filesystem_.PRead(sfd.get(), buf.get(), buf_size,
+                                pre_mapping_file_offset),
+              Eq(buf_size));
   EXPECT_THAT(buf.get()[0], Eq('a'));
 }
 
diff --git a/icing/file/mock-filesystem.h b/icing/file/mock-filesystem.h
index 32817d4..02740f6 100644
--- a/icing/file/mock-filesystem.h
+++ b/icing/file/mock-filesystem.h
@@ -299,15 +299,15 @@
                size_t data_size),
               (const));
 
-  MOCK_METHOD(bool, Read, (int fd, void* buf, size_t buf_size), (const));
+  MOCK_METHOD(ssize_t, Read, (int fd, void* buf, size_t buf_size), (const));
 
-  MOCK_METHOD(bool, Read, (const char* filename, void* buf, size_t buf_size),
+  MOCK_METHOD(ssize_t, Read, (const char* filename, void* buf, size_t buf_size),
               (const));
 
-  MOCK_METHOD(bool, PRead, (int fd, void* buf, size_t buf_size, off_t offset),
-              (const));
+  MOCK_METHOD(ssize_t, PRead,
+              (int fd, void* buf, size_t buf_size, off_t offset), (const));
 
-  MOCK_METHOD(bool, PRead,
+  MOCK_METHOD(ssize_t, PRead,
               (const char* filename, void* buf, size_t buf_size, off_t offset),
               (const));
 
diff --git a/icing/file/persistent-hash-map.cc b/icing/file/persistent-hash-map.cc
index 148c7ea..3daeadd 100644
--- a/icing/file/persistent-hash-map.cc
+++ b/icing/file/persistent-hash-map.cc
@@ -29,6 +29,7 @@
 #include "icing/file/memory-mapped-file.h"
 #include "icing/file/persistent-storage.h"
 #include "icing/util/crc32.h"
+#include "icing/util/logging.h"
 #include "icing/util/status-macros.h"
 
 namespace icing {
@@ -504,8 +505,13 @@
 
   // Magic should be the same.
   if (persistent_hash_map->info().magic != Info::kMagic) {
+    ICING_LOG(ERROR) << "Invalid header magic for PersistentHashMap "
+                     << persistent_hash_map->working_path_
+                     << ". Expected: " << Info::kMagic
+                     << ", actual: " << persistent_hash_map->info().magic;
     return absl_ports::FailedPreconditionError(
-        "PersistentHashMap header magic mismatch");
+        absl_ports::StrCat("Invalid header magic for PersistentHashMap: ",
+                           persistent_hash_map->working_path_));
   }
 
   // Value type size should be consistent.
diff --git a/icing/file/persistent-hash-map_test.cc b/icing/file/persistent-hash-map_test.cc
index d2e211d..6accfbb 100644
--- a/icing/file/persistent-hash-map_test.cc
+++ b/icing/file/persistent-hash-map_test.cc
@@ -235,8 +235,9 @@
 
   // Check info section
   Info info;
-  ASSERT_TRUE(filesystem_.PRead(metadata_sfd.get(), &info, sizeof(Info),
-                                PersistentHashMap::kInfoMetadataFileOffset));
+  ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), &info, sizeof(Info),
+                                PersistentHashMap::kInfoMetadataFileOffset),
+              Eq(sizeof(Info)));
   EXPECT_THAT(info.magic, Eq(Info::kMagic));
   EXPECT_THAT(info.value_type_size, Eq(sizeof(int)));
   EXPECT_THAT(info.max_load_factor_percent,
@@ -246,8 +247,9 @@
 
   // Check crcs section
   Crcs crcs;
-  ASSERT_TRUE(filesystem_.PRead(metadata_sfd.get(), &crcs, sizeof(Crcs),
-                                PersistentHashMap::kCrcsMetadataFileOffset));
+  ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), &crcs, sizeof(Crcs),
+                                PersistentHashMap::kCrcsMetadataFileOffset),
+              Eq(sizeof(Crcs)));
   // # of elements in bucket_storage should be 1, so it should have non-zero
   // all storages crc value.
   EXPECT_THAT(crcs.component_crcs.storages_crc, Ne(0));
@@ -487,12 +489,14 @@
     ASSERT_TRUE(metadata_sfd.is_valid());
 
     Crcs crcs;
-    ASSERT_TRUE(filesystem_.PRead(metadata_sfd.get(), &crcs, sizeof(Crcs),
-                                  PersistentHashMap::kCrcsMetadataFileOffset));
+    ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), &crcs, sizeof(Crcs),
+                                  PersistentHashMap::kCrcsMetadataFileOffset),
+                Eq(sizeof(Crcs)));
 
     Info info;
-    ASSERT_TRUE(filesystem_.PRead(metadata_sfd.get(), &info, sizeof(Info),
-                                  PersistentHashMap::kInfoMetadataFileOffset));
+    ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), &info, sizeof(Info),
+                                  PersistentHashMap::kInfoMetadataFileOffset),
+                Eq(sizeof(Info)));
 
     // Manually change magic and update checksums.
     info.magic += kCorruptedValueOffset;
@@ -515,7 +519,7 @@
     EXPECT_THAT(persistent_hash_map_or,
                 StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
     EXPECT_THAT(persistent_hash_map_or.status().error_message(),
-                HasSubstr("PersistentHashMap header magic mismatch"));
+                HasSubstr("Invalid header magic for PersistentHashMap"));
   }
 }
 
@@ -612,8 +616,9 @@
   ASSERT_TRUE(metadata_sfd.is_valid());
 
   Crcs crcs;
-  ASSERT_TRUE(filesystem_.PRead(metadata_sfd.get(), &crcs, sizeof(Crcs),
-                                PersistentHashMap::kCrcsMetadataFileOffset));
+  ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), &crcs, sizeof(Crcs),
+                                PersistentHashMap::kCrcsMetadataFileOffset),
+              Eq(sizeof(Crcs)));
 
   // Manually corrupt all_crc
   crcs.all_crc += kCorruptedValueOffset;
@@ -656,8 +661,9 @@
   ASSERT_TRUE(metadata_sfd.is_valid());
 
   Info info;
-  ASSERT_TRUE(filesystem_.PRead(metadata_sfd.get(), &info, sizeof(Info),
-                                PersistentHashMap::kInfoMetadataFileOffset));
+  ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), &info, sizeof(Info),
+                                PersistentHashMap::kInfoMetadataFileOffset),
+              Eq(sizeof(Info)));
 
   // Modify info, but don't update the checksum. This would be similar to
   // corruption of info.
@@ -863,8 +869,9 @@
   ASSERT_TRUE(metadata_sfd.is_valid());
 
   Info info;
-  ASSERT_TRUE(filesystem_.PRead(metadata_sfd.get(), &info, sizeof(Info),
-                                PersistentHashMap::kInfoMetadataFileOffset));
+  ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), &info, sizeof(Info),
+                                PersistentHashMap::kInfoMetadataFileOffset),
+              Eq(sizeof(Info)));
   EXPECT_THAT(info.max_load_factor_percent,
               Eq(options.max_load_factor_percent));
 
diff --git a/icing/file/portable-file-backed-proto-log.h b/icing/file/portable-file-backed-proto-log.h
index b498563..6188171 100644
--- a/icing/file/portable-file-backed-proto-log.h
+++ b/icing/file/portable-file-backed-proto-log.h
@@ -53,6 +53,7 @@
 #ifndef ICING_FILE_PORTABLE_FILE_BACKED_PROTO_LOG_H_
 #define ICING_FILE_PORTABLE_FILE_BACKED_PROTO_LOG_H_
 
+#include <algorithm>
 #include <cstddef>
 #include <cstdint>
 #include <cstring>
@@ -60,11 +61,11 @@
 #include <string>
 #include <string_view>
 #include <utility>
-#include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/canonical_errors.h"
+#include "icing/absl_ports/mutex.h"
 #include "icing/absl_ports/str_cat.h"
 #include "icing/file/constants.h"
 #include "icing/file/filesystem.h"
@@ -73,8 +74,9 @@
 #include "icing/portable/endian.h"
 #include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
-#include "icing/portable/zlib.h"
+#include "icing/proto/persist.pb.h"
 #include "icing/util/bit-util.h"
+#include "icing/util/clock.h"
 #include "icing/util/crc32.h"
 #include "icing/util/data-loss.h"
 #include "icing/util/logging.h"
@@ -111,20 +113,56 @@
     // BEST_COMPRESSION = 9
     const int32_t compression_level;
 
+    // The threshold in bytes for compression if enabled. If the proto is larger
+    // than or equal to this threshold, it will be compressed.
+    const uint32_t compression_threshold_bytes;
+
+    // Level of memory usage for compression if enabled, BEST_MEMORY = 1,
+    // BEST_COMPRESSION and SPEED = 9
+    const int32_t compression_mem_level;
+
+    // Whether to use a smaller decompression buffer size. If false, the
+    // decompression buffer size will be the default size of 64MiB.
+    const bool enable_smaller_decompression_buffer_size;
+
+    // Whether to enable the new header format and unsynced tail checksum
+    // related changes.
+    const bool enable_new_header_format;
+
+    // Whether to retain a decompression buffer that reads can reuse rather than
+    // allocating a new one for each read.
+    const bool enable_reusable_decompression_buffer;
+
     // Must specify values for options.
     Options() = delete;
-    explicit Options(
-        bool compress_in,
-        const int32_t max_proto_size_in = constants::kMaxProtoSize,
-        const int32_t compression_level_in = kDefaultCompressionLevel)
+    explicit Options(bool compress_in, const int32_t max_proto_size_in,
+                     const int32_t compression_level_in,
+                     const uint32_t compression_threshold_bytes_in,
+                     const int32_t compression_mem_level_in,
+                     const bool enable_smaller_decompression_buffer_size_in,
+                     const bool enable_new_header_format_in,
+                     const bool enable_reusable_decompression_buffer_in)
         : compress(compress_in),
           max_proto_size(max_proto_size_in),
-          compression_level(compression_level_in) {}
+          compression_level(compression_level_in),
+          compression_threshold_bytes(compression_threshold_bytes_in),
+          compression_mem_level(compression_mem_level_in),
+          enable_smaller_decompression_buffer_size(
+              enable_smaller_decompression_buffer_size_in),
+          enable_new_header_format(enable_new_header_format_in),
+          enable_reusable_decompression_buffer(
+              enable_reusable_decompression_buffer_in) {}
   };
 
   // Level of compression, BEST_SPEED = 1, BEST_COMPRESSION = 9
   static constexpr int kDefaultCompressionLevel = 3;
 
+  // The default compression threshold is 0, which means always compress.
+  static constexpr uint32_t kDefaultCompressionThresholdBytes = 0;
+
+  // The compression ratio to use for decompression buffer size.
+  static constexpr int kProtoCompressionRatio = 3;
+
   // Number of bytes we reserve for the heading at the beginning of the proto
   // log. We reserve this so the header can grow without running into the
   // contents of the proto log, triggering an unnecessary migration of the data.
@@ -140,21 +178,58 @@
     // format.
     static constexpr int32_t kFileFormatVersion = 0;
 
-    uint32_t CalculateHeaderChecksum() const {
+    // Legacy header section padding offset.
+    // - In the legacy header definition, we never declared the padding bytes.
+    //   Instead, it was implicitly added by C++ compiler.
+    // - In order to make the legacy header checksum calculation compatible, we
+    //   need to declare the padding bytes explicitly before the new fields.
+    static constexpr int32_t kLegacyHeaderSectionPaddingOffset =
+        offsetof(Header, reserved_padding1);
+
+    // Legacy header section size, including reserved_padding1. This size must
+    // not be changed due to compatibility issues.
+    static constexpr int32_t kLegacyHeaderSectionSize = 32;
+
+    uint32_t CalculateLegacyHeaderChecksum() const {
+      static_assert(offsetof(Header, flags_) == 28,
+                    "Incompatible header layout for flags and paddings!");
+      static_assert(kLegacyHeaderSectionPaddingOffset +
+                        sizeof(reserved_padding1) ==
+                    kLegacyHeaderSectionSize);
+
       Crc32 crc;
 
-      // Get a string_view of all the fields of the Header, excluding the
-      // magic_nbytes_ and header_checksum_nbytes_
-      std::string_view header_str(
+      // Get a string_view of all the fields of the legacy header section,
+      // excluding the magic_nbytes_ and legacy_header_checksum_nbytes_.
+      std::string_view legacy_header_str(
           reinterpret_cast<const char*>(this) +
-              offsetof(Header, header_checksum_nbytes_) +
-              sizeof(header_checksum_nbytes_),
-          sizeof(Header) - sizeof(magic_nbytes_) -
-              sizeof(header_checksum_nbytes_));
-      crc.Append(header_str);
+              offsetof(Header, legacy_header_checksum_nbytes_) +
+              sizeof(legacy_header_checksum_nbytes_),
+          kLegacyHeaderSectionSize - sizeof(magic_nbytes_) -
+              sizeof(legacy_header_checksum_nbytes_));
+      crc.Append(legacy_header_str);
       return crc.Get();
     }
 
+    Crc32 CalculateHeaderChecksum() const {
+      // The header_checksum field must be at offset 252.
+      static_assert(offsetof(Header, header_checksum_nbytes_) == 252);
+
+      // Get a string_view of all the fields of the header, excluding
+      // header_checksum_nbytes_ itself.
+      std::string_view header_str(
+          reinterpret_cast<const char*>(this),
+          sizeof(Header) - sizeof(header_checksum_nbytes_));
+      return Crc32(header_str);
+    }
+
+    void UpdateHeaderChecksums(bool enable_new_header_format) {
+      SetLegacyHeaderChecksum(CalculateLegacyHeaderChecksum());
+      if (enable_new_header_format) {
+        SetHeaderChecksum(CalculateHeaderChecksum().Get());
+      }
+    }
+
     int32_t GetMagic() const { return GNetworkToHostL(magic_nbytes_); }
 
     void SetMagic(int32_t magic_in) {
@@ -193,12 +268,13 @@
       rewind_offset_nbytes_ = GHostToNetworkLL(rewind_offset_in);
     }
 
-    int32_t GetHeaderChecksum() const {
-      return GNetworkToHostL(header_checksum_nbytes_);
+    uint32_t GetLegacyHeaderChecksum() const {
+      return GNetworkToHostL(legacy_header_checksum_nbytes_);
     }
 
-    void SetHeaderChecksum(int32_t header_checksum_in) {
-      header_checksum_nbytes_ = GHostToNetworkL(header_checksum_in);
+    void SetLegacyHeaderChecksum(uint32_t legacy_header_checksum_in) {
+      legacy_header_checksum_nbytes_ =
+          GHostToNetworkL(legacy_header_checksum_in);
     }
 
     bool GetCompressFlag() const { return GetFlag(kCompressBit); }
@@ -209,6 +285,35 @@
 
     void SetDirtyFlag(bool dirty) { SetFlag(kDirtyBit, dirty); }
 
+    uint32_t GetUnsyncedTailChecksum() const {
+      return GNetworkToHostL(unsynced_tail_checksum_nbytes_);
+    }
+
+    void SetUnsyncedTailChecksum(uint32_t unsynced_tail_checksum_in) {
+      unsynced_tail_checksum_nbytes_ =
+          GHostToNetworkL(unsynced_tail_checksum_in);
+    }
+
+    // Resets paddings to 0.
+    //
+    // Normally a new header will have default 0 bytes for all paddings since we
+    // declared "= {0}" for them, but if the header was read from an existing
+    // file generated by older versions, the paddings may contain some random
+    // values. Therefore, InitializeExistingFile will reset the paddings to 0
+    // just in case.
+    void ResetPaddings() {
+      memset(reserved_padding1, 0, sizeof(reserved_padding1));
+      memset(available_padding, 0, sizeof(available_padding));
+    }
+
+    uint32_t GetHeaderChecksum() const {
+      return GNetworkToHostL(header_checksum_nbytes_);
+    }
+
+    void SetHeaderChecksum(uint32_t header_checksum_in) {
+      header_checksum_nbytes_ = GHostToNetworkL(header_checksum_in);
+    }
+
    private:
     // The least-significant bit offset at which the compress flag is stored in
     // 'flags_nbytes_'. Represents whether the protos in the log are compressed
@@ -228,16 +333,55 @@
       bit_util::BitfieldSet(value, offset, /*len=*/1, &flags_);
     }
 
+    // Header bytes layout:
+    //             +--------- 4 bytes ---------+--------- 4 bytes ---------+
+    // offset 0    |           magic           |  legacy_header_checksum   |
+    // offset 8    |                     rewind_offset                     |
+    // offset 16   |    file_format_version    |      max_proto_size       |
+    // offset 24   |       log_checksum        | flags | reserved_padding1 |
+    //
+    // <legacy header section: offset 0 to 31>
+    //
+    // offset 32   |  unsynced_tail_checksum   |    available_padding[]    |
+    // offset 40   |                  available_padding[]                  |
+    // ...
+    // offset 240  |                  available_padding[]                  |
+    // offset 248  |    available_padding[]    |      header_checksum      |
+    //             +--------- 4 bytes ---------+--------- 4 bytes ---------+
+    //
+    // Note:
+    // - Due to compatibility issues, legacy header checksum must always be
+    //   computed by the fixed legacy header section, and the legacy header
+    //   section must not be changed in the future.
+    // - The new "header_checksum" field is added to the end of the header
+    //   section, and it is computed by the entire header section (excluding
+    //   itself), i.e. offset 0 to 251.
+    // - Any new fields can be added into the section of available_padding, and
+    //   the developer must:
+    //   - Maintain the fields and essential paddings according to the C++
+    //     struct bytes layout and padding rules
+    //   - Recompute the available_padding size to make sizeof(Header) == 256
+    //     and header_checksum_nbytes_ remain at the end of the header (i.e.
+    //     offset 252-255).
+
+    //// START OF LEGACY HEADER SECTION ////
+
     // Holds the magic as a quick sanity check against file corruption.
     //
     // Field is in network-byte order.
     int32_t magic_nbytes_ = GHostToNetworkL(kMagic);
 
     // Must be at the beginning after kMagic. Contains the crc checksum of
-    // the following fields.
+    // the entire legacy header section excluding magic and itself (i.e. offset
+    // 8 to 31).
+    //
+    // Need to keep this field and it should be computed by the same way
+    // forever, due to Android mainline rollback policy. We have to maintain
+    // "forward compatibility", i.e. data generated by the new code should be
+    // able to be interpreted by the old code.
     //
     // Field is in network-byte order.
-    uint32_t header_checksum_nbytes_ = 0;
+    uint32_t legacy_header_checksum_nbytes_ = 0;
 
     // Last known good offset at which the log and its checksum were updated.
     // If we crash between writing to the log and updating the checksum, we can
@@ -272,12 +416,30 @@
     // Field is only 1 byte, so is byte-order agnostic.
     uint8_t flags_ = 0;
 
-    // NOTE: New fields should *almost always* be added to the end here. Since
-    // this class may have already been written to disk, appending fields
-    // increases the chances that changes are backwards-compatible.
+    // Reserved padding after flags_ for the C++ struct bytes layout.
+    // Due to compatibility with legacy_header_checksum_nbytes_, we have to pad
+    // the legacy header section to 32 bytes.
+    uint8_t reserved_padding1[3] = {0};
+
+    //// END OF LEGACY HEADER SECTION ////
+
+    // Checksum of the unsynced tail elements.
+    //
+    // Field is in network-byte order.
+    uint32_t unsynced_tail_checksum_nbytes_ = 0;
+
+    // Available bytes for future use.
+    uint8_t available_padding[216] = {0};
+
+    // Must be at the end of the header, i.e. offset 252-255. Contains the crc
+    // checksum of the entire header section excluding itself (i.e. offset 0 to
+    // 251).
+    //
+    // Field is in network-byte order.
+    uint32_t header_checksum_nbytes_ = 0;
   };
-  static_assert(sizeof(Header) <= kHeaderReservedBytes,
-                "Header has grown past our reserved bytes!");
+  static_assert(sizeof(Header) == kHeaderReservedBytes,
+                "Header size should be the same as the reserved bytes!");
 
   struct CreateResult {
     // A successfully initialized log.
@@ -381,6 +543,9 @@
   //   INTERNAL_ERROR on IO error
   libtextclassifier3::StatusOr<int64_t> GetElementsFileSize() const;
 
+  // For testing only.
+  Header* header() { return header_.get(); }
+
   // An iterator helping to find offsets of all the protos in file.
   // Example usage:
   //
@@ -480,7 +645,9 @@
   // Returns:
   //   OK on success
   //   INTERNAL_ERROR on IO error
-  libtextclassifier3::Status PersistToDisk();
+  libtextclassifier3::Status PersistToDisk(
+      PersistToDiskStatsProto* persist_stats = nullptr,
+      const Clock* clock = nullptr);
 
   // Calculates the checksum of the log contents (excluding the header) and
   // updates the header.
@@ -500,10 +667,13 @@
 
  private:
   // Object can only be instantiated via the ::Create factory.
-  PortableFileBackedProtoLog(const Filesystem* filesystem,
-                             const std::string& file_path,
-                             std::unique_ptr<Header> header, int64_t file_size,
-                             int32_t compression_level);
+  explicit PortableFileBackedProtoLog(
+      const Filesystem* filesystem, const std::string& file_path,
+      std::unique_ptr<Header> header, int64_t file_size,
+      int32_t compression_level, uint32_t compression_threshold_bytes,
+      int32_t compression_mem_level,
+      bool enable_smaller_decompression_buffer_size,
+      bool enable_new_header_format, bool enable_reusable_decompression_buffer);
 
   // Initializes a new proto log.
   //
@@ -553,9 +723,9 @@
   // metadata and converts it into a portable metadata before writing.
   //
   // Returns:
-  //   OK on success
+  //   Crc32 of the written metadata bytes on success
   //   INTERNAL_ERROR on any IO errors
-  static libtextclassifier3::Status WriteProtoMetadata(
+  static libtextclassifier3::StatusOr<Crc32> WriteProtoMetadata(
       const Filesystem* filesystem, int fd, int32_t host_order_metadata);
 
   static bool IsEmptyBuffer(const char* buffer, int size) {
@@ -571,6 +741,30 @@
   // Metadata format: 8 bits magic + 24 bits size
   static uint8_t GetProtoMagic(int metadata) { return metadata >> 24; }
 
+  class BufferHolder {
+   public:
+    BufferHolder(const PortableFileBackedProtoLog* log,
+                 std::unique_ptr<uint8_t[]> buffer, size_t size)
+        : log_(log), buffer_(std::move(buffer)), size_(size) {}
+
+    ~BufferHolder() { log_->ReturnBuffer(std::move(buffer_), size_); }
+
+    uint8_t* data() { return buffer_.get(); }
+    size_t size() const { return size_; }
+
+   private:
+    const PortableFileBackedProtoLog* log_;
+    std::unique_ptr<uint8_t[]> buffer_;
+    size_t size_;
+  };
+
+  // Returns the buffer to the log. It will either be cached or destroyed.
+  void ReturnBuffer(std::unique_ptr<uint8_t[]> buffer, size_t size) const;
+
+  // Returns a buffer holder to the caller - either a newly constructed one or
+  // the cached one if it is big enough.
+  BufferHolder PossiblyBorrowBuffer(size_t size) const;
+
   // Magic number added in front of every proto. Used when reading out protos
   // as a first check for corruption in each entry in the file. Even if there is
   // a corruption, the best we can do is roll back to our last recovery point
@@ -589,18 +783,38 @@
   std::unique_ptr<Header> header_;
   int64_t file_size_;
   const int32_t compression_level_;
+  const uint32_t compression_threshold_bytes_;
+  const int32_t compression_mem_level_;
+  const bool enable_smaller_decompression_buffer_size_;
+  const bool enable_new_header_format_;
+  const bool enable_reusable_decompression_buffer_;
+
+  mutable std::unique_ptr<uint8_t[]> read_buffer_ ICING_GUARDED_BY(mutex_);
+  mutable size_t read_buffer_size_ ICING_GUARDED_BY(mutex_);
+  mutable absl_ports::shared_mutex mutex_;
 };
 
 template <typename ProtoT>
 PortableFileBackedProtoLog<ProtoT>::PortableFileBackedProtoLog(
     const Filesystem* filesystem, const std::string& file_path,
     std::unique_ptr<Header> header, int64_t file_size,
-    int32_t compression_level)
+    int32_t compression_level, uint32_t compression_threshold_bytes,
+    int32_t compression_mem_level,
+    bool enable_smaller_decompression_buffer_size,
+    bool enable_new_header_format, bool enable_reusable_decompression_buffer)
     : filesystem_(filesystem),
       file_path_(file_path),
       header_(std::move(header)),
       file_size_(file_size),
-      compression_level_(compression_level) {
+      compression_level_(compression_level),
+      compression_threshold_bytes_(compression_threshold_bytes),
+      compression_mem_level_(compression_mem_level),
+      enable_smaller_decompression_buffer_size_(
+          enable_smaller_decompression_buffer_size),
+      enable_new_header_format_(enable_new_header_format),
+      enable_reusable_decompression_buffer_(
+          enable_reusable_decompression_buffer),
+      read_buffer_size_(0) {
   fd_.reset(filesystem_->OpenForAppend(file_path.c_str()));
 }
 
@@ -639,6 +853,13 @@
         options.compression_level));
   }
 
+  if (options.compression_mem_level < 1 || options.compression_mem_level > 9) {
+    return absl_ports::InvalidArgumentError(
+        IcingStringUtil::StringPrintf("options.compression_mem_level must be "
+                                      "between 1 and 9 inclusive, was %d",
+                                      options.compression_mem_level));
+  }
+
   if (!filesystem->FileExists(file_path.c_str())) {
     return InitializeNewFile(filesystem, file_path, options);
   }
@@ -668,22 +889,50 @@
         absl_ports::StrCat("Failed to initialize file size: ", file_path));
   }
 
-  // Create the header
+  // Create the header.
+  //
+  // No need to memset padding bytes to 0 since we declared = {0} in the struct
+  // definition and the new object created by std::make_unique will initialize
+  // them to 0.
   std::unique_ptr<Header> header = std::make_unique<Header>();
   header->SetCompressFlag(options.compress);
   header->SetMaxProtoSize(options.max_proto_size);
-  header->SetHeaderChecksum(header->CalculateHeaderChecksum());
+  header->UpdateHeaderChecksums(options.enable_new_header_format);
 
-  if (!filesystem->Write(file_path.c_str(), header.get(), sizeof(Header))) {
-    return absl_ports::InternalError(
-        absl_ports::StrCat("Failed to write header for file: ", file_path));
+  {
+    ScopedFd fd(filesystem->OpenForWrite(file_path.c_str()));
+    if (!fd.is_valid()) {
+      return absl_ports::InternalError(
+          absl_ports::StrCat("Failed to open file for write: ", file_path));
+    }
+
+    if (!filesystem->Write(fd.get(), header.get(), sizeof(Header))) {
+      return absl_ports::InternalError(
+          absl_ports::StrCat("Failed to write header for file: ", file_path));
+    }
+
+    // Sync the file to disk to ensure that the header is flushed to disk.
+    // - Otherwise, if the app crashes before the header is flushed, the next
+    //   initialize may fail.
+    // - This is especially important for this class, since it is used to store
+    //   ground truth data. If magic or checksum is wrong, then Icing cannot
+    //   recover it from this state and therefore end up with entire data loss.
+    if (!filesystem->DataSync(fd.get())) {
+      return absl_ports::InternalError(
+          absl_ports::StrCat("Failed to sync file: ", file_path));
+    }
   }
 
   CreateResult create_result = {
       std::unique_ptr<PortableFileBackedProtoLog<ProtoT>>(
           new PortableFileBackedProtoLog<ProtoT>(
               filesystem, file_path, std::move(header),
-              /*file_size=*/kHeaderReservedBytes, options.compression_level)),
+              /*file_size=*/kHeaderReservedBytes, options.compression_level,
+              options.compression_threshold_bytes,
+              options.compression_mem_level,
+              options.enable_smaller_decompression_buffer_size,
+              options.enable_new_header_format,
+              options.enable_reusable_decompression_buffer)),
       /*data_loss=*/DataLoss::NONE, /*recalculated_checksum=*/false};
 
   return create_result;
@@ -695,30 +944,62 @@
 PortableFileBackedProtoLog<ProtoT>::InitializeExistingFile(
     const Filesystem* filesystem, const std::string& file_path,
     const Options& options, int64_t file_size) {
+  bool legacy_header_section_changed = false;
   bool header_changed = false;
   if (file_size < kHeaderReservedBytes) {
+    ICING_LOG(ERROR) << "Invalid file size for PortableFileBackedProtoLog "
+                     << file_path
+                     << " for header reserved bytes. File size: " << file_size
+                     << ", header reserved bytes: " << kHeaderReservedBytes;
     return absl_ports::InternalError(
         absl_ports::StrCat("File header too short for: ", file_path));
   }
 
   std::unique_ptr<Header> header = std::make_unique<Header>();
-  if (!filesystem->PRead(file_path.c_str(), header.get(), sizeof(Header),
-                         /*offset=*/0)) {
+  if (filesystem->PRead(file_path.c_str(), header.get(), sizeof(Header),
+                        /*offset=*/0) != sizeof(Header)) {
     return absl_ports::InternalError(
         absl_ports::StrCat("Failed to read header for file: ", file_path));
   }
 
   // Make sure the header is still valid before we use any of its values. This
-  // is covered by the header_checksum check below, but this is a quick check
-  // that can save us from an extra crc computation.
+  // is covered by the legacy_header_checksum check below, but this is a quick
+  // check that can save us from an extra crc computation.
   if (header->GetMagic() != Header::kMagic) {
-    return absl_ports::InternalError(
-        absl_ports::StrCat("Invalid header kMagic for file: ", file_path));
+    ICING_LOG(ERROR) << "Invalid header magic for PortableFileBackedProtoLog "
+                     << file_path << ". Expected: " << Header::kMagic
+                     << ", actual: " << header->GetMagic();
+    return absl_ports::InternalError(absl_ports::StrCat(
+        "Invalid header magic for PortableFileBackedProtoLog: ", file_path));
   }
 
-  if (header->GetHeaderChecksum() != header->CalculateHeaderChecksum()) {
+  // Check the new header checksum.
+  bool header_checksum_mismatch = false;
+  if (options.enable_new_header_format) {
+    header_checksum_mismatch =
+        header->GetHeaderChecksum() != header->CalculateHeaderChecksum().Get();
+    if (header_checksum_mismatch) {
+      // If upgrade or rollforward happens, then existing data's header_checksum
+      // may not be written by the old code, so we need to fallback to legacy
+      // checksum.
+      ICING_LOG(WARNING)
+          << "Invalid header checksum for PortableFileBackedProtoLog "
+          << file_path
+          << ". Fallback to legacy header checksum validation, and need to "
+             "rewind unsynced tail.";
+    }
+  }
+
+  // Check the legacy header checksum.
+  if (header->GetLegacyHeaderChecksum() !=
+      header->CalculateLegacyHeaderChecksum()) {
+    // Old code and new code should write legacy header checksum correctly, so
+    // if it doesn't match, then it's likely that the file is corrupted.
+    ICING_LOG(ERROR)
+        << "Invalid legacy header checksum for PortableFileBackedProtoLog "
+        << file_path;
     return absl_ports::InternalError(
-        absl_ports::StrCat("Invalid header checksum for: ", file_path));
+        absl_ports::StrCat("Invalid legacy header checksum for: ", file_path));
   }
 
   if (header->GetRewindOffset() < kHeaderReservedBytes) {
@@ -749,21 +1030,55 @@
     // It's fine if our new max size is greater than our previous one. Existing
     // data is still valid.
     header->SetMaxProtoSize(options.max_proto_size);
-    header_changed = true;
+    legacy_header_section_changed = true;
   }
 
   DataLoss data_loss = DataLoss::NONE;
 
-  // If we have any documents in our tail, get rid of them since they're not in
-  // our checksum. Our checksum reflects content up to the rewind offset.
+  // If we have any documents in our tail, validate the unsynced tail checksum
+  // and get rid of the unsynced tail if they don't match.
   if (file_size > header->GetRewindOffset()) {
-    if (!filesystem->Truncate(file_path.c_str(), header->GetRewindOffset())) {
-      return absl_ports::InternalError(IcingStringUtil::StringPrintf(
-          "Failed to truncate '%s' to size %lld", file_path.data(),
-          static_cast<long long>(header->GetRewindOffset())));
+    // - If enable_new_header_format is false, then we need to throw away all
+    //   data in the unsynced tail.
+    // - Otherwise we use the new header format logic:
+    //   - If the header checksum doesn't match, then we need to throw away all
+    //     data in the unsynced tail, regardless of the unsynced tail checksum.
+    //   - Otherwise, validate the unsynced tail checksum to determine if we
+    //     need to throw away all data in the unsynced tail.
+    bool need_rewind_unsynced_tail =
+        !options.enable_new_header_format || header_checksum_mismatch;
+    if (!need_rewind_unsynced_tail) {
+      // Recompute the unsynced tail's checksum only if the header checksum
+      // matches. This saves us a crc computation if the header checksum already
+      // doesn't match.
+      ICING_ASSIGN_OR_RETURN(
+          Crc32 calculated_unsynced_tail_checksum,
+          GetPartialChecksum(filesystem, file_path, Crc32(),
+                             /*start=*/header->GetRewindOffset(),
+                             /*end=*/file_size, file_size));
+      if (header->GetUnsyncedTailChecksum() !=
+          calculated_unsynced_tail_checksum.Get()) {
+        // Unsynced tail checksum doesn't match, so we need to throw it out.
+        ICING_LOG(WARNING)
+            << "Mismatch unsynced tail checksum. Expected (from data): "
+            << calculated_unsynced_tail_checksum.Get()
+            << ", actual (stored in the header): "
+            << header->GetUnsyncedTailChecksum();
+        need_rewind_unsynced_tail = true;
+      }
     }
-    file_size = header->GetRewindOffset();
-    data_loss = DataLoss::PARTIAL;
+
+    if (need_rewind_unsynced_tail) {
+      if (!filesystem->Truncate(file_path.c_str(), header->GetRewindOffset())) {
+        return absl_ports::InternalError(IcingStringUtil::StringPrintf(
+            "Failed to truncate '%s' to size %lld", file_path.data(),
+            static_cast<long long>(header->GetRewindOffset())));
+      }
+      header->SetUnsyncedTailChecksum(0);
+      file_size = header->GetRewindOffset();
+      data_loss = DataLoss::PARTIAL;
+      header_changed = true;
+    }
   }
 
   bool recalculated_checksum = false;
@@ -801,11 +1116,15 @@
 
     // Update our header.
     header->SetDirtyFlag(false);
-    header_changed = true;
+    legacy_header_section_changed = true;
   }
 
-  if (header_changed) {
-    header->SetHeaderChecksum(header->CalculateHeaderChecksum());
+  if (legacy_header_section_changed || header_changed ||
+      header_checksum_mismatch) {
+    // Reset all padding bytes to 0 before recompute the checksums, just in
+    // case we had some random values generated by the old code.
+    header->ResetPaddings();
+    header->UpdateHeaderChecksums(options.enable_new_header_format);
 
     if (!filesystem->PWrite(file_path.c_str(), /*offset=*/0, header.get(),
                             sizeof(Header))) {
@@ -816,9 +1135,13 @@
 
   CreateResult create_result = {
       std::unique_ptr<PortableFileBackedProtoLog<ProtoT>>(
-          new PortableFileBackedProtoLog<ProtoT>(filesystem, file_path,
-                                                 std::move(header), file_size,
-                                                 options.compression_level)),
+          new PortableFileBackedProtoLog<ProtoT>(
+              filesystem, file_path, std::move(header), file_size,
+              options.compression_level, options.compression_threshold_bytes,
+              options.compression_mem_level,
+              options.enable_smaller_decompression_buffer_size,
+              options.enable_new_header_format,
+              options.enable_reusable_decompression_buffer)),
       data_loss, recalculated_checksum};
 
   return create_result;
@@ -829,12 +1152,6 @@
 PortableFileBackedProtoLog<ProtoT>::GetPartialChecksum(
     const Filesystem* filesystem, const std::string& file_path,
     Crc32 initial_crc, int64_t start, int64_t end, int64_t file_size) {
-  ICING_ASSIGN_OR_RETURN(
-      MemoryMappedFile mmapped_file,
-      MemoryMappedFile::Create(*filesystem, file_path,
-                               MemoryMappedFile::Strategy::READ_ONLY));
-  Crc32 new_crc(initial_crc.Get());
-
   if (start < 0) {
     return absl_ports::InvalidArgumentError(IcingStringUtil::StringPrintf(
         "Starting checksum offset of file '%s' must be greater than 0, was "
@@ -858,6 +1175,16 @@
         static_cast<long long>(end)));
   }
 
+  if (start == end) {
+    return initial_crc;
+  }
+
+  ICING_ASSIGN_OR_RETURN(
+      MemoryMappedFile mmapped_file,
+      MemoryMappedFile::Create(*filesystem, file_path,
+                               MemoryMappedFile::Strategy::READ_ONLY));
+  Crc32 new_crc(initial_crc.Get());
+
   Architecture architecture = GetArchitecture();
   switch (architecture) {
     case Architecture::BIT_64: {
@@ -922,7 +1249,17 @@
   if (header_->GetCompressFlag()) {
     protobuf_ports::GzipOutputStream::Options options;
     options.format = protobuf_ports::GzipOutputStream::ZLIB;
-    options.compression_level = compression_level_;
+
+    if (proto_size >= compression_threshold_bytes_) {
+      options.compression_level = compression_level_;
+    } else {
+      options.compression_level = 0;
+    }
+    options.buffer_size = proto_size;
+    if (proto_size < 0 ||
+        static_cast<size_t>(proto_size) > protobuf_ports::kDefaultBufferSize) {
+      options.buffer_size = protobuf_ports::kDefaultBufferSize;
+    }
 
     protobuf_ports::GzipOutputStream compressing_stream(&proto_stream, options);
 
@@ -955,7 +1292,8 @@
 
   // Actually write metadata, has to be done after we know the possibly
   // compressed proto size
-  ICING_RETURN_IF_ERROR(
+  ICING_ASSIGN_OR_RETURN(
+      Crc32 metadata_crc,
       WriteProtoMetadata(filesystem_, fd_.get(), host_order_metadata));
 
   // Write the serialized proto
@@ -967,6 +1305,23 @@
   // Update file size. The file should have grown by sizeof(Metadata) + size of
   // the serialized proto.
   file_size_ += sizeof(host_order_metadata) + final_size;
+
+  if (enable_new_header_format_) {
+    // Compute the new unsynced tail checksum.
+    Crc32 new_unsynced_tail_crc(header_->GetUnsyncedTailChecksum());
+    new_unsynced_tail_crc.Combine(metadata_crc, sizeof(host_order_metadata));
+    new_unsynced_tail_crc.Append(proto_str);
+
+    // Set the new unsynced tail checksum and header checksum. Write the header.
+    header_->SetUnsyncedTailChecksum(new_unsynced_tail_crc.Get());
+    header_->SetHeaderChecksum(header_->CalculateHeaderChecksum().Get());
+    if (!filesystem_->PWrite(fd_.get(), /*offset=*/0, header_.get(),
+                             sizeof(Header))) {
+      return absl_ports::InternalError(absl_ports::StrCat(
+          "Failed to update header after write proto: ", file_path_));
+    }
+  }
+
   return current_position;
 }
 
@@ -990,7 +1345,8 @@
                                       static_cast<long long>(file_size_ - 1)));
   }
   auto buf = std::make_unique<char[]>(stored_size);
-  if (!filesystem_->PRead(fd_.get(), buf.get(), stored_size, file_offset)) {
+  if (filesystem_->PRead(fd_.get(), buf.get(), stored_size, file_offset) !=
+      stored_size) {
     return absl_ports::InternalError("");
   }
 
@@ -1003,7 +1359,15 @@
   // Deserialize proto
   ProtoT proto;
   if (header_->GetCompressFlag()) {
-    protobuf_ports::GzipInputStream decompress_stream(&proto_stream);
+    size_t buffer_size = protobuf_ports::kDefaultBufferSize;
+    if (enable_smaller_decompression_buffer_size_ && stored_size >= 0) {
+      buffer_size = std::min(buffer_size, kProtoCompressionRatio *
+                                              static_cast<size_t>(stored_size));
+    }
+    BufferHolder buffer_holder = PossiblyBorrowBuffer(buffer_size);
+    protobuf_ports::GzipInputStream decompress_stream(
+        &proto_stream, protobuf_ports::GzipInputStream::AUTO,
+        buffer_holder.data(), buffer_holder.size());
     proto.ParseFromZeroCopyStream(&decompress_stream);
   } else {
     proto.ParseFromZeroCopyStream(&proto_stream);
@@ -1030,28 +1394,44 @@
   }
   auto buf = std::make_unique<char[]>(stored_size);
 
-  // We need to update the crc checksum if the erased area is before the
-  // rewind position.
-  int32_t new_crc;
+  // Update the checksum depending on whether the erased area is before or after
+  // rewind offset.
+  uint32_t new_crc = 0;  // Reused for both cases.
   if (file_offset < header_->GetRewindOffset()) {
+    // Case 1: erasing bytes from log checksummed area (before rewind offset).
+    //
+    // kHeaderReservedBytes                     RewindOffset           file_size
+    //          v                                    v                     v
+    // +--------+------------------------------------+---------------------+
+    // | Header |        Log checksummed area        |    Unsynced tail    |
+    // +--------+------------------------------------+---------------------+
+    //          |<-          log_checksum          ->|<-   ut_checksum   ->|
+    //                 xxxxxxxxxxxx
+    //                 ^           ^
+    //            file_offset file_offset
+    //                          + stored_size
+
     // Set to "dirty" before we start writing anything.
     header_->SetDirtyFlag(true);
-    header_->SetHeaderChecksum(header_->CalculateHeaderChecksum());
+    header_->UpdateHeaderChecksums(enable_new_header_format_);
+
     if (!filesystem_->PWrite(fd_.get(), /*offset=*/0, header_.get(),
                              sizeof(Header))) {
       return absl_ports::InternalError(absl_ports::StrCat(
           "Failed to update dirty bit of header to: ", file_path_));
     }
 
-    // We need to calculate [original string xor 0s].
-    // The xored string is the same as the original string because 0 xor 0 =
-    // 0, 1 xor 0 = 1.
     // Read the compressed proto out.
-    if (!filesystem_->PRead(fd_.get(), buf.get(), stored_size, file_offset)) {
-      return absl_ports::InternalError("");
+    if (filesystem_->PRead(fd_.get(), buf.get(), stored_size, file_offset) !=
+        stored_size) {
+      return absl_ports::InternalError("Cannot read proto from file");
     }
+    // We're going to erase the bytes. For example, "ABCDEF" -> "A\0\0DEF". The
+    // checksum can be easily updated by ("BC" XOR "\0\0"), which remains "BC",
+    // so the XORed string is the original string.
     const std::string_view xored_str(buf.get(), stored_size);
 
+    // Recompute the log checksum by the XORed data.
     Crc32 crc(header_->GetLogChecksum());
     ICING_ASSIGN_OR_RETURN(
         new_crc,
@@ -1059,6 +1439,42 @@
                           /*full_data_size=*/header_->GetRewindOffset() -
                               kHeaderReservedBytes,
                           /*position=*/file_offset - kHeaderReservedBytes));
+  } else {
+    // Case 2: erasing bytes from unsynced tail checksum area (after rewind
+    // offset).
+    //
+    // kHeaderReservedBytes                     RewindOffset           file_size
+    //          v                                    v                     v
+    // +--------+------------------------------------+---------------------+
+    // | Header |        Log checksummed area        |    Unsynced tail    |
+    // +--------+------------------------------------+---------------------+
+    //          |<-          log_checksum          ->|<-   ut_checksum   ->|
+    //                                                    xxxxxxxxxxxx
+    //                                                    ^           ^
+    //                                               file_offset file_offset
+    //                                                             + stored_size
+
+    // Read the compressed proto out and re-calculate the unsynced tail checksum
+    // only if the flag is enabled.
+    if (enable_new_header_format_) {
+      if (filesystem_->PRead(fd_.get(), buf.get(), stored_size, file_offset) !=
+          stored_size) {
+        return absl_ports::InternalError("Cannot read proto from file");
+      }
+      // We're going to erase the bytes. For example, "ABCDEF" -> "A\0\0DEF".
+      // The checksum can be easily updated by ("BC" XOR "\0\0"), which remains
+      // "BC", so the XORed string is the original string.
+      const std::string_view xored_str(buf.get(), stored_size);
+
+      // Recompute the unsynced tail checksum by the XORed data.
+      Crc32 crc(header_->GetUnsyncedTailChecksum());
+      ICING_ASSIGN_OR_RETURN(
+          new_crc,
+          crc.UpdateWithXor(
+              xored_str,
+              /*full_data_size=*/file_size_ - header_->GetRewindOffset(),
+              /*position=*/file_offset - header_->GetRewindOffset()));
+    }
   }
 
   // Clear the region.
@@ -1067,18 +1483,30 @@
     return absl_ports::InternalError("");
   }
 
-  // If we cleared something in our checksummed area, we should update our
-  // checksum and reset our dirty bit.
+  // Update header.
+  bool write_header = false;
   if (file_offset < header_->GetRewindOffset()) {
+    // If we cleared something in the log checksummed area, we should reset the
+    // dirty bit and update related all checksums.
     header_->SetDirtyFlag(false);
     header_->SetLogChecksum(new_crc);
-    header_->SetHeaderChecksum(header_->CalculateHeaderChecksum());
+    header_->UpdateHeaderChecksums(enable_new_header_format_);
 
-    if (!filesystem_->PWrite(fd_.get(), /*offset=*/0, header_.get(),
-                             sizeof(Header))) {
-      return absl_ports::InternalError(
-          absl_ports::StrCat("Failed to update header to: ", file_path_));
-    }
+    write_header = true;
+  } else if (enable_new_header_format_) {
+    // If we cleared something in the unsynced tail section, we should update
+    // the unsynced tail checksum and header checksum only if the flag is
+    // enabled.
+    header_->SetUnsyncedTailChecksum(new_crc);
+    header_->SetHeaderChecksum(header_->CalculateHeaderChecksum().Get());
+
+    write_header = true;
+  }
+
+  if (write_header && !filesystem_->PWrite(fd_.get(), /*offset=*/0,
+                                           header_.get(), sizeof(Header))) {
+    return absl_ports::InternalError(
+        absl_ports::StrCat("Failed to update header to: ", file_path_));
   }
 
   return libtextclassifier3::Status::OK;
@@ -1168,7 +1596,8 @@
         static_cast<long long>(file_size)));
   }
 
-  if (!filesystem->PRead(fd, &portable_metadata, metadata_size, file_offset)) {
+  if (filesystem->PRead(fd, &portable_metadata, metadata_size, file_offset) !=
+      metadata_size) {
     return absl_ports::InternalError("");
   }
 
@@ -1187,7 +1616,7 @@
 }
 
 template <typename ProtoT>
-libtextclassifier3::Status
+libtextclassifier3::StatusOr<Crc32>
 PortableFileBackedProtoLog<ProtoT>::WriteProtoMetadata(
     const Filesystem* filesystem, int fd, int32_t host_order_metadata) {
   // Convert it into portable endian format before writing to disk
@@ -1200,18 +1629,37 @@
         absl_ports::StrCat("Failed to write proto metadata."));
   }
 
-  return libtextclassifier3::Status::OK;
+  std::string_view metadata_bytes(
+      reinterpret_cast<const char*>(&portable_metadata),
+      sizeof(portable_metadata));
+  return Crc32(metadata_bytes);
 }
 
 template <typename ProtoT>
-libtextclassifier3::Status PortableFileBackedProtoLog<ProtoT>::PersistToDisk() {
+libtextclassifier3::Status PortableFileBackedProtoLog<ProtoT>::PersistToDisk(
+    PersistToDiskStatsProto* persist_stats, const Clock* clock) {
   if (file_size_ == header_->GetRewindOffset()) {
     // No new protos appended, don't need to update the checksum.
     return libtextclassifier3::Status::OK;
   }
 
+  std::unique_ptr<Timer> persist_timer;
+  if (persist_stats != nullptr && clock != nullptr) {
+    persist_timer = clock->GetNewTimer();
+  }
   ICING_RETURN_IF_ERROR(UpdateChecksum());
-  if (!filesystem_->DataSync(fd_.get())) {
+  if (persist_stats != nullptr && clock != nullptr) {
+    persist_stats->set_document_log_checksum_update_latency_ms(
+        persist_timer->GetElapsedMilliseconds());
+    persist_timer = clock->GetNewTimer();
+  }
+
+  bool datasync_succeeded = filesystem_->DataSync(fd_.get());
+  if (persist_stats != nullptr && clock != nullptr) {
+    persist_stats->set_document_log_data_sync_latency_ms(
+        persist_timer->GetElapsedMilliseconds());
+  }
+  if (!datasync_succeeded) {
     return absl_ports::InternalError(
         absl_ports::StrCat("Failed to sync data to disk: ", file_path_));
   }
@@ -1227,8 +1675,9 @@
   }
   ICING_ASSIGN_OR_RETURN(Crc32 crc, GetChecksum());
   header_->SetLogChecksum(crc.Get());
+  header_->SetUnsyncedTailChecksum(0);
   header_->SetRewindOffset(file_size_);
-  header_->SetHeaderChecksum(header_->CalculateHeaderChecksum());
+  header_->UpdateHeaderChecksums(enable_new_header_format_);
 
   if (!filesystem_->PWrite(fd_.get(), /*offset=*/0, header_.get(),
                            sizeof(Header))) {
@@ -1251,6 +1700,13 @@
                               /*start=*/kHeaderReservedBytes,
                               /*end=*/file_size_, file_size_);
   } else {
+    if (enable_new_header_format_) {
+      // Combine unsynced tail checksum with log checksum.
+      Crc32 log_checksum(header_->GetLogChecksum());
+      Crc32 unsynced_tail_checksum(header_->GetUnsyncedTailChecksum());
+      log_checksum.Combine(unsynced_tail_checksum, new_content_size);
+      return log_checksum;
+    }
     // Append new changes to the existing checksum.
     return GetPartialChecksum(
         filesystem_, file_path_, Crc32(header_->GetLogChecksum()),
@@ -1258,6 +1714,36 @@
   }
 }
 
+template <typename ProtoT>
+void PortableFileBackedProtoLog<ProtoT>::ReturnBuffer(
+    std::unique_ptr<uint8_t[]> buffer, size_t size) const {
+  if (!enable_reusable_decompression_buffer_) {
+    return;
+  }
+  absl_ports::unique_lock lock(&mutex_);
+  if (read_buffer_ == nullptr || read_buffer_size_ < size) {
+    read_buffer_.swap(buffer);
+    read_buffer_size_ = size;
+  }
+}
+
+template <typename ProtoT>
+typename PortableFileBackedProtoLog<ProtoT>::BufferHolder
+PortableFileBackedProtoLog<ProtoT>::PossiblyBorrowBuffer(size_t size) const {
+  if (!enable_reusable_decompression_buffer_) {
+    return BufferHolder(this, std::make_unique<uint8_t[]>(size), size);
+  }
+  absl_ports::unique_lock lock(&mutex_);
+  if (read_buffer_ == nullptr || read_buffer_size_ < size) {
+    return BufferHolder(this, std::make_unique<uint8_t[]>(size), size);
+  }
+  std::unique_ptr<uint8_t[]> temp;
+  temp.swap(read_buffer_);
+  size_t temp_size = read_buffer_size_;
+  read_buffer_size_ = 0;
+  return BufferHolder(this, std::move(temp), temp_size);
+}
+
 }  // namespace lib
 }  // namespace icing
 
diff --git a/icing/file/portable-file-backed-proto-log_benchmark.cc b/icing/file/portable-file-backed-proto-log_benchmark.cc
index e6935ab..36ab812 100644
--- a/icing/file/portable-file-backed-proto-log_benchmark.cc
+++ b/icing/file/portable-file-backed-proto-log_benchmark.cc
@@ -13,14 +13,16 @@
 // limitations under the License.
 
 #include <cstdint>
+#include <memory>
 #include <random>
+#include <string>
 
 #include "testing/base/public/benchmark.h"
-#include "gmock/gmock.h"
 #include "icing/document-builder.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/legacy/core/icing-string-util.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/proto/document.pb.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/random-string.h"
@@ -55,6 +57,25 @@
 
 namespace {
 
+std::unique_ptr<PortableFileBackedProtoLog<DocumentProto>> CreateProtoLog(
+    const Filesystem& filesystem, const std::string& file_path,
+    int max_proto_size, bool compress) {
+  return PortableFileBackedProtoLog<DocumentProto>::Create(
+             &filesystem, file_path,
+             PortableFileBackedProtoLog<DocumentProto>::Options(
+                 compress, max_proto_size,
+                 PortableFileBackedProtoLog<
+                     DocumentProto>::kDefaultCompressionLevel,
+                 PortableFileBackedProtoLog<
+                     DocumentProto>::kDefaultCompressionThresholdBytes,
+                 protobuf_ports::kDefaultMemLevel,
+                 /*enable_smaller_decompression_buffer_size=*/true,
+                 /*enable_new_header_format=*/true,
+                 /*enable_reusable_decompression_buffer=*/true))
+      .ValueOrDie()
+      .proto_log;
+}
+
 void BM_Write(benchmark::State& state) {
   const Filesystem filesystem;
   int string_length = state.range(0);
@@ -66,12 +87,8 @@
   // Make sure it doesn't already exist.
   filesystem.DeleteFile(file_path.c_str());
 
-  auto proto_log = PortableFileBackedProtoLog<DocumentProto>::Create(
-                       &filesystem, file_path,
-                       PortableFileBackedProtoLog<DocumentProto>::Options(
-                           compress, max_proto_size))
-                       .ValueOrDie()
-                       .proto_log;
+  auto proto_log =
+      CreateProtoLog(filesystem, file_path, max_proto_size, compress);
 
   DocumentProto document = DocumentBuilder().SetKey("namespace", "uri").Build();
 
@@ -119,12 +136,8 @@
   // Make sure it doesn't already exist.
   filesystem.DeleteFile(file_path.c_str());
 
-  auto proto_log = PortableFileBackedProtoLog<DocumentProto>::Create(
-                       &filesystem, file_path,
-                       PortableFileBackedProtoLog<DocumentProto>::Options(
-                           compress, max_proto_size))
-                       .ValueOrDie()
-                       .proto_log;
+  auto proto_log =
+      CreateProtoLog(filesystem, file_path, max_proto_size, compress);
 
   DocumentProto document = DocumentBuilder().SetKey("namespace", "uri").Build();
 
@@ -174,12 +187,8 @@
   // Make sure it doesn't already exist.
   filesystem.DeleteFile(file_path.c_str());
 
-  auto proto_log = PortableFileBackedProtoLog<DocumentProto>::Create(
-                       &filesystem, file_path,
-                       PortableFileBackedProtoLog<DocumentProto>::Options(
-                           compress, max_proto_size))
-                       .ValueOrDie()
-                       .proto_log;
+  auto proto_log =
+      CreateProtoLog(filesystem, file_path, max_proto_size, compress);
 
   DocumentProto document = DocumentBuilder().SetKey("namespace", "uri").Build();
 
@@ -213,12 +222,8 @@
   // Make sure it doesn't already exist.
   filesystem.DeleteFile(file_path.c_str());
 
-  auto proto_log = PortableFileBackedProtoLog<DocumentProto>::Create(
-                       &filesystem, file_path,
-                       PortableFileBackedProtoLog<DocumentProto>::Options(
-                           compress, max_proto_size))
-                       .ValueOrDie()
-                       .proto_log;
+  auto proto_log =
+      CreateProtoLog(filesystem, file_path, max_proto_size, compress);
 
   DocumentProto document = DocumentBuilder().SetKey("namespace", "uri").Build();
 
@@ -255,12 +260,8 @@
   // Make sure it doesn't already exist.
   filesystem.DeleteFile(file_path.c_str());
 
-  auto proto_log = PortableFileBackedProtoLog<DocumentProto>::Create(
-                       &filesystem, file_path,
-                       PortableFileBackedProtoLog<DocumentProto>::Options(
-                           compress, max_proto_size))
-                       .ValueOrDie()
-                       .proto_log;
+  auto proto_log =
+      CreateProtoLog(filesystem, file_path, max_proto_size, compress);
 
   DocumentProto document = DocumentBuilder().SetKey("namespace", "uri").Build();
 
@@ -299,12 +300,8 @@
   // Make sure it doesn't already exist.
   filesystem.DeleteFile(file_path.c_str());
 
-  auto proto_log = PortableFileBackedProtoLog<DocumentProto>::Create(
-                       &filesystem, file_path,
-                       PortableFileBackedProtoLog<DocumentProto>::Options(
-                           compress, max_proto_size))
-                       .ValueOrDie()
-                       .proto_log;
+  auto proto_log =
+      CreateProtoLog(filesystem, file_path, max_proto_size, compress);
 
   DocumentProto document = DocumentBuilder().SetKey("namespace", "uri").Build();
 
diff --git a/icing/file/portable-file-backed-proto-log_test.cc b/icing/file/portable-file-backed-proto-log_test.cc
index 587bfdc..97dbbe6 100644
--- a/icing/file/portable-file-backed-proto-log_test.cc
+++ b/icing/file/portable-file-backed-proto-log_test.cc
@@ -16,16 +16,27 @@
 
 #include <cstdint>
 #include <cstdlib>
+#include <cstring>
+#include <memory>
+#include <string>
+#include <utility>
 
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "icing/document-builder.h"
 #include "icing/file/filesystem.h"
+#include "icing/file/memory-mapped-file.h"
 #include "icing/file/mock-filesystem.h"
 #include "icing/portable/equals-proto.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/proto/document.pb.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/crc32.h"
+#include "icing/util/data-loss.h"
+#include "icing/util/status-macros.h"
 
 namespace icing {
 namespace lib {
@@ -33,14 +44,12 @@
 namespace {
 
 using ::icing::lib::portable_equals_proto::EqualsProto;
-using ::testing::A;
 using ::testing::Eq;
 using ::testing::Gt;
 using ::testing::HasSubstr;
+using ::testing::Ne;
 using ::testing::Not;
 using ::testing::NotNull;
-using ::testing::Pair;
-using ::testing::Return;
 
 using Header = PortableFileBackedProtoLog<DocumentProto>::Header;
 
@@ -56,7 +65,34 @@
   filesystem.Write(file_path.c_str(), &header, sizeof(Header));
 }
 
-class PortableFileBackedProtoLogTest : public ::testing::Test {
+libtextclassifier3::StatusOr<std::pair<int64_t, int64_t>>
+WriteProtoAndReturnOffsetAndSize(
+    PortableFileBackedProtoLog<DocumentProto>* proto_log,
+    const DocumentProto& document) {
+  ICING_ASSIGN_OR_RETURN(int64_t document_log_size_before,
+                         proto_log->GetElementsFileSize());
+  ICING_ASSIGN_OR_RETURN(int64_t offset, proto_log->WriteProto(document));
+  ICING_ASSIGN_OR_RETURN(int64_t document_log_size_after,
+                         proto_log->GetElementsFileSize());
+  int64_t size_written = document_log_size_after - document_log_size_before;
+  return std::make_pair(offset, size_written);
+}
+
+void CheckNewChecksums(const Header& header,
+                       uint32_t expected_unsynced_tail_checksum,
+                       uint32_t expected_header_checksum,
+                       bool enable_new_header_format) {
+  if (enable_new_header_format) {
+    EXPECT_THAT(header.GetUnsyncedTailChecksum(),
+                Eq(expected_unsynced_tail_checksum));
+    EXPECT_THAT(header.GetHeaderChecksum(), Eq(expected_header_checksum));
+  } else {
+    EXPECT_THAT(header.GetUnsyncedTailChecksum(), Eq(0u));
+    EXPECT_THAT(header.GetHeaderChecksum(), Eq(0u));
+  }
+}
+
+class PortableFileBackedProtoLogTest : public ::testing::TestWithParam<bool> {
  protected:
   // Adds a user-defined default construct because a const member variable may
   // make the compiler accidentally delete the default constructor.
@@ -75,16 +111,24 @@
   bool compress_ = true;
   int32_t compression_level_ =
       PortableFileBackedProtoLog<DocumentProto>::kDefaultCompressionLevel;
+  uint32_t compression_threshold_bytes_ = PortableFileBackedProtoLog<
+      DocumentProto>::kDefaultCompressionThresholdBytes;
+  int32_t compression_mem_level_ = protobuf_ports::kDefaultMemLevel;
   int64_t max_proto_size_ = 256 * 1024;  // 256 KiB
+  bool enable_smaller_decompression_buffer_size_ = true;
 };
 
-TEST_F(PortableFileBackedProtoLogTest, Initialize) {
+TEST_P(PortableFileBackedProtoLogTest, Initialize) {
   ICING_ASSERT_OK_AND_ASSIGN(
       PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
       PortableFileBackedProtoLog<DocumentProto>::Create(
           &filesystem_, file_path_,
           PortableFileBackedProtoLog<DocumentProto>::Options(
-              compress_, max_proto_size_, compression_level_)));
+              compress_, max_proto_size_, compression_level_,
+              compression_threshold_bytes_, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
   EXPECT_THAT(create_result.proto_log, NotNull());
   EXPECT_FALSE(create_result.has_data_loss());
   EXPECT_FALSE(create_result.recalculated_checksum);
@@ -93,17 +137,47 @@
   ASSERT_THAT(PortableFileBackedProtoLog<DocumentProto>::Create(
                   &filesystem_, file_path_,
                   PortableFileBackedProtoLog<DocumentProto>::Options(
-                      !compress_, max_proto_size_, compression_level_)),
+                      !compress_, max_proto_size_, compression_level_,
+                      compression_threshold_bytes_, compression_mem_level_,
+                      enable_smaller_decompression_buffer_size_,
+                      /*enable_new_header_format_in=*/GetParam(),
+                      /*enable_reusable_decompression_buffer=*/true)),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
-TEST_F(PortableFileBackedProtoLogTest, InitializeValidatesOptions) {
+TEST_P(PortableFileBackedProtoLogTest, NewAndEmptyFileShouldFlushHeader) {
+  // Mock the filesystem to verify that DataSync is called.
+  auto mock_filesystem = std::make_unique<MockFilesystem>();
+  EXPECT_CALL(*mock_filesystem, DataSync(_)).Times(1);
+
+  {
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            mock_filesystem.get(), file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    EXPECT_THAT(create_result.proto_log, NotNull());
+    EXPECT_FALSE(create_result.has_data_loss());
+    EXPECT_FALSE(create_result.recalculated_checksum);
+  }
+}
+
+TEST_P(PortableFileBackedProtoLogTest, InitializeValidatesOptions) {
   // max_proto_size must be greater than 0
   int invalid_max_proto_size = 0;
   ASSERT_THAT(PortableFileBackedProtoLog<DocumentProto>::Create(
                   &filesystem_, file_path_,
                   PortableFileBackedProtoLog<DocumentProto>::Options(
-                      compress_, invalid_max_proto_size, compression_level_)),
+                      compress_, invalid_max_proto_size, compression_level_,
+                      compression_threshold_bytes_, compression_mem_level_,
+                      enable_smaller_decompression_buffer_size_,
+                      /*enable_new_header_format_in=*/GetParam(),
+                      /*enable_reusable_decompression_buffer=*/true)),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
   // max_proto_size must be under 16 MiB
@@ -111,7 +185,11 @@
   ASSERT_THAT(PortableFileBackedProtoLog<DocumentProto>::Create(
                   &filesystem_, file_path_,
                   PortableFileBackedProtoLog<DocumentProto>::Options(
-                      compress_, invalid_max_proto_size, compression_level_)),
+                      compress_, invalid_max_proto_size, compression_level_,
+                      compression_threshold_bytes_, compression_mem_level_,
+                      enable_smaller_decompression_buffer_size_,
+                      /*enable_new_header_format_in=*/GetParam(),
+                      /*enable_reusable_decompression_buffer=*/true)),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
   // compression_level must be between 0 and 9 inclusive
@@ -119,7 +197,11 @@
   ASSERT_THAT(PortableFileBackedProtoLog<DocumentProto>::Create(
                   &filesystem_, file_path_,
                   PortableFileBackedProtoLog<DocumentProto>::Options(
-                      compress_, max_proto_size_, invalid_compression_level)),
+                      compress_, max_proto_size_, invalid_compression_level,
+                      compression_threshold_bytes_, compression_mem_level_,
+                      enable_smaller_decompression_buffer_size_,
+                      /*enable_new_header_format_in=*/GetParam(),
+                      /*enable_reusable_decompression_buffer=*/true)),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
   // compression_level must be between 0 and 9 inclusive
@@ -127,17 +209,51 @@
   ASSERT_THAT(PortableFileBackedProtoLog<DocumentProto>::Create(
                   &filesystem_, file_path_,
                   PortableFileBackedProtoLog<DocumentProto>::Options(
-                      compress_, max_proto_size_, invalid_compression_level)),
+                      compress_, max_proto_size_, invalid_compression_level,
+                      compression_threshold_bytes_, compression_mem_level_,
+                      enable_smaller_decompression_buffer_size_,
+                      /*enable_new_header_format_in=*/GetParam(),
+                      /*enable_reusable_decompression_buffer=*/true)),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+
+  // compression_mem_level must be between 1 and 9 inclusive
+  int invalid_compression_mem_level = 0;
+  ASSERT_THAT(
+      PortableFileBackedProtoLog<DocumentProto>::Create(
+          &filesystem_, file_path_,
+          PortableFileBackedProtoLog<DocumentProto>::Options(
+              compress_, max_proto_size_, compression_level_,
+              compression_threshold_bytes_, invalid_compression_mem_level,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+
+  // compression_mem_level must be between 1 and 9 inclusive
+  invalid_compression_mem_level = 10;
+  ASSERT_THAT(
+      PortableFileBackedProtoLog<DocumentProto>::Create(
+          &filesystem_, file_path_,
+          PortableFileBackedProtoLog<DocumentProto>::Options(
+              compress_, max_proto_size_, compression_level_,
+              compression_threshold_bytes_, invalid_compression_mem_level,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
-TEST_F(PortableFileBackedProtoLogTest, ReservedSpaceForHeader) {
+TEST_P(PortableFileBackedProtoLogTest, ReservedSpaceForHeader) {
   ICING_ASSERT_OK_AND_ASSIGN(
       PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
       PortableFileBackedProtoLog<DocumentProto>::Create(
           &filesystem_, file_path_,
           PortableFileBackedProtoLog<DocumentProto>::Options(
-              compress_, max_proto_size_, compression_level_)));
+              compress_, max_proto_size_, compression_level_,
+              compression_threshold_bytes_, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
 
   // With no protos written yet, the log should be minimum the size of the
   // reserved header space.
@@ -145,14 +261,18 @@
             PortableFileBackedProtoLog<DocumentProto>::kHeaderReservedBytes);
 }
 
-TEST_F(PortableFileBackedProtoLogTest, WriteProtoTooLarge) {
+TEST_P(PortableFileBackedProtoLogTest, WriteProtoTooLarge) {
   int max_proto_size = 1;
   ICING_ASSERT_OK_AND_ASSIGN(
       PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
       PortableFileBackedProtoLog<DocumentProto>::Create(
           &filesystem_, file_path_,
           PortableFileBackedProtoLog<DocumentProto>::Options(
-              compress_, max_proto_size, compression_level_)));
+              compress_, max_proto_size, compression_level_,
+              compression_threshold_bytes_, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
   auto proto_log = std::move(create_result.proto_log);
   ASSERT_FALSE(create_result.has_data_loss());
 
@@ -163,13 +283,17 @@
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
-TEST_F(PortableFileBackedProtoLogTest, ReadProtoWrongKProtoMagic) {
+TEST_P(PortableFileBackedProtoLogTest, ReadProtoWrongKProtoMagic) {
   ICING_ASSERT_OK_AND_ASSIGN(
       PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
       PortableFileBackedProtoLog<DocumentProto>::Create(
           &filesystem_, file_path_,
           PortableFileBackedProtoLog<DocumentProto>::Options(
-              compress_, max_proto_size_, compression_level_)));
+              compress_, max_proto_size_, compression_level_,
+              compression_threshold_bytes_, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
   auto proto_log = std::move(create_result.proto_log);
   ASSERT_FALSE(create_result.has_data_loss());
 
@@ -195,7 +319,7 @@
               StatusIs(libtextclassifier3::StatusCode::INTERNAL));
 }
 
-TEST_F(PortableFileBackedProtoLogTest, ReadWriteUncompressedProto) {
+TEST_P(PortableFileBackedProtoLogTest, ReadWriteUncompressedProto) {
   int last_offset;
   {
     ICING_ASSERT_OK_AND_ASSIGN(
@@ -203,7 +327,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                /*compress_in=*/false, max_proto_size_, compression_level_)));
+                /*compress_in=*/false, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -250,7 +378,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                /*compress_in=*/false, max_proto_size_, compression_level_)));
+                /*compress_in=*/false, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto recreated_proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -263,7 +395,7 @@
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest, ReadWriteCompressedProto) {
+TEST_P(PortableFileBackedProtoLogTest, ReadWriteCompressedProto) {
   int last_offset;
 
   {
@@ -272,7 +404,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                /*compress_in=*/true, max_proto_size_, compression_level_)));
+                /*compress_in=*/true, max_proto_size_, compression_level_,
+                /*compression_threshold_bytes_in=*/0, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -319,7 +455,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                /*compress_in=*/true, max_proto_size_, compression_level_)));
+                /*compress_in=*/true, max_proto_size_, compression_level_,
+                /*compression_threshold_bytes_in=*/0, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto recreated_proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -332,7 +472,7 @@
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest, ReadWriteDifferentCompressionLevel) {
+TEST_P(PortableFileBackedProtoLogTest, ReadWriteDifferentCompressionLevel) {
   int document1_offset;
   int document2_offset;
   int document3_offset;
@@ -356,7 +496,11 @@
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
                 /*compress_in=*/true, max_proto_size_,
-                /*compression_level_in=*/3)));
+                /*compression_level_in=*/3,
+                /*compression_threshold_bytes_in=*/0, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -381,7 +525,11 @@
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
                 /*compress_in=*/true, max_proto_size_,
-                /*compression_level_in=*/9)));
+                /*compression_level_in=*/9,
+                /*compression_threshold_bytes_in=*/0, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto recreated_proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -410,7 +558,11 @@
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
                 /*compress_in=*/true, max_proto_size_,
-                /*compression_level=*/0)));
+                /*compression_level_in=*/0,
+                /*compression_threshold_bytes_in=*/0, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto recreated_proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -434,7 +586,233 @@
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest,
+TEST_P(PortableFileBackedProtoLogTest, ReadWriteDifferentCompressionMemLevel) {
+  int document1_offset;
+  int document2_offset;
+  int document3_offset;
+
+  // The first proto to write that's close to the max size. Leave some room for
+  // the rest of the proto properties.
+  std::string long_str(max_proto_size_ - 1024, 'a');
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("namespace1", "uri1")
+                                .AddStringProperty("long_str", long_str)
+                                .Build();
+  DocumentProto document2 =
+      DocumentBuilder().SetKey("namespace2", "uri2").Build();
+  DocumentProto document3 =
+      DocumentBuilder().SetKey("namespace3", "uri3").Build();
+
+  {
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                /*compress_in=*/true, max_proto_size_, compression_level_,
+                compression_threshold_bytes_,
+                /*compression_mem_level_in=*/8,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto proto_log = std::move(create_result.proto_log);
+    ASSERT_FALSE(create_result.has_data_loss());
+
+    // Write the first proto
+    ICING_ASSERT_OK_AND_ASSIGN(document1_offset,
+                               proto_log->WriteProto(document1));
+
+    // Check that what we read is what we wrote
+    ASSERT_THAT(proto_log->ReadProto(document1_offset),
+                IsOkAndHolds(EqualsProto(document1)));
+
+    ICING_ASSERT_OK(proto_log->PersistToDisk());
+  }
+
+  // Make a new proto_log with the same file_path but different compression mem
+  // level, and make sure we can still read from and write to the same
+  // underlying file.
+  {
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                /*compress_in=*/true, max_proto_size_, compression_level_,
+                compression_threshold_bytes_,
+                /*compression_mem_level_in=*/1,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto recreated_proto_log = std::move(create_result.proto_log);
+    ASSERT_FALSE(create_result.has_data_loss());
+
+    // Check the first proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document1_offset),
+                IsOkAndHolds(EqualsProto(document1)));
+
+    // Write a second proto
+    ICING_ASSERT_OK_AND_ASSIGN(document2_offset,
+                               recreated_proto_log->WriteProto(document2));
+
+    ASSERT_GT(document2_offset, document1_offset);
+
+    // Check the second proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document2_offset),
+                IsOkAndHolds(EqualsProto(document2)));
+
+    ICING_ASSERT_OK(recreated_proto_log->PersistToDisk());
+  }
+
+  // One more time but with 9 compression mem level
+  {
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                /*compress_in=*/true, max_proto_size_, compression_level_,
+                compression_threshold_bytes_,
+                /*compression_mem_level_in=*/9,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto recreated_proto_log = std::move(create_result.proto_log);
+    ASSERT_FALSE(create_result.has_data_loss());
+
+    // Check the first proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document1_offset),
+                IsOkAndHolds(EqualsProto(document1)));
+
+    // Check the second proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document2_offset),
+                IsOkAndHolds(EqualsProto(document2)));
+
+    // Write a third proto
+    ICING_ASSERT_OK_AND_ASSIGN(document3_offset,
+                               recreated_proto_log->WriteProto(document3));
+
+    ASSERT_GT(document3_offset, document2_offset);
+
+    // Check the third proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document3_offset),
+                IsOkAndHolds(EqualsProto(document3)));
+  }
+}
+
+TEST_P(PortableFileBackedProtoLogTest,
+       ReadWriteEnableAndDisableSmallerDecompressionBufferSize) {
+  int document1_offset;
+  int document2_offset;
+  int document3_offset;
+
+  // The first proto to write that's close to the max size. Leave some room for
+  // the rest of the proto properties.
+  std::string long_str(max_proto_size_ - 1024, 'a');
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("namespace1", "uri1")
+                                .AddStringProperty("long_str", long_str)
+                                .Build();
+  DocumentProto document2 =
+      DocumentBuilder().SetKey("namespace2", "uri2").Build();
+  DocumentProto document3 =
+      DocumentBuilder().SetKey("namespace3", "uri3").Build();
+
+  {
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                /*compress_in=*/true, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                /*enable_smaller_decompression_buffer_size_in=*/false,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto proto_log = std::move(create_result.proto_log);
+    ASSERT_FALSE(create_result.has_data_loss());
+
+    // Write the first proto
+    ICING_ASSERT_OK_AND_ASSIGN(document1_offset,
+                               proto_log->WriteProto(document1));
+
+    // Check that what we read is what we wrote
+    ASSERT_THAT(proto_log->ReadProto(document1_offset),
+                IsOkAndHolds(EqualsProto(document1)));
+
+    ICING_ASSERT_OK(proto_log->PersistToDisk());
+  }
+
+  // Make a new proto_log with the same file_path with smaller decompression
+  // buffer size enabled, and make sure we can still read from and write to the
+  // same underlying file.
+  {
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                /*compress_in=*/true, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                /*enable_smaller_decompression_buffer_size_in=*/true,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto recreated_proto_log = std::move(create_result.proto_log);
+    ASSERT_FALSE(create_result.has_data_loss());
+
+    // Check the first proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document1_offset),
+                IsOkAndHolds(EqualsProto(document1)));
+
+    // Write a second proto
+    ICING_ASSERT_OK_AND_ASSIGN(document2_offset,
+                               recreated_proto_log->WriteProto(document2));
+
+    ASSERT_GT(document2_offset, document1_offset);
+
+    // Check the second proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document2_offset),
+                IsOkAndHolds(EqualsProto(document2)));
+
+    ICING_ASSERT_OK(recreated_proto_log->PersistToDisk());
+  }
+
+  // One more time but with smaller decompression buffer size disabled
+  {
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                /*compress_in=*/true, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                /*enable_smaller_decompression_buffer_size_in=*/false,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto recreated_proto_log = std::move(create_result.proto_log);
+    ASSERT_FALSE(create_result.has_data_loss());
+
+    // Check the first proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document1_offset),
+                IsOkAndHolds(EqualsProto(document1)));
+
+    // Check the second proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document2_offset),
+                IsOkAndHolds(EqualsProto(document2)));
+
+    // Write a third proto
+    ICING_ASSERT_OK_AND_ASSIGN(document3_offset,
+                               recreated_proto_log->WriteProto(document3));
+
+    ASSERT_GT(document3_offset, document2_offset);
+
+    // Check the third proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document3_offset),
+                IsOkAndHolds(EqualsProto(document3)));
+  }
+}
+
+TEST_P(PortableFileBackedProtoLogTest,
        WriteDifferentCompressionLevelDifferentSizes) {
   int document_log_size_with_compression_3;
   int document_log_size_with_no_compression;
@@ -454,7 +832,11 @@
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
                 /*compress_in=*/true, max_proto_size_,
-                /*compression_level_in=*/3)));
+                /*compression_level_in=*/3, compression_threshold_bytes_,
+                compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -476,7 +858,11 @@
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
                 /*compress_in=*/true, max_proto_size_,
-                /*compression_level_in=*/0)));
+                /*compression_level_in=*/0, compression_threshold_bytes_,
+                compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -494,44 +880,505 @@
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest, CorruptHeader) {
+TEST_P(PortableFileBackedProtoLogTest, CompressionThreshold) {
+  // Create document1 with size of 1024 bytes.
+  int64_t document1_size = 1024;
+  int64_t document1_offset;
+  DocumentProto document1 =
+      DocumentBuilder()
+          .SetKey("namespace1", "uri1")
+          .AddStringProperty("long_str", std::string(990, 'a'))
+          .Build();
+  ASSERT_THAT(document1.ByteSizeLong(), Eq(document1_size));
+
+  // Create document2 with size of 900 bytes.
+  int64_t document2_size = 900;
+  int64_t document2_offset;
+  DocumentProto document2 =
+      DocumentBuilder()
+          .SetKey("namespace1", "uri1")
+          .AddStringProperty("long_str", std::string(866, 'a'))
+          .Build();
+  ASSERT_THAT(document2.ByteSizeLong(), Eq(document2_size));
+
+  {
+    // Create a proto_log with compression threshold of 1000 bytes
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                /*compress_in=*/true, max_proto_size_,
+                /*compression_level_in=*/3,
+                /*compression_threshold_bytes_in=*/1000, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto proto_log = std::move(create_result.proto_log);
+    ASSERT_FALSE(create_result.has_data_loss());
+
+    ICING_ASSERT_OK_AND_ASSIGN(
+        auto document1_write_result,
+        WriteProtoAndReturnOffsetAndSize(proto_log.get(), document1));
+    document1_offset = document1_write_result.first;
+    int64_t document1_size_written = document1_write_result.second;
+    // Check that what we read is what we wrote
+    ASSERT_THAT(proto_log->ReadProto(document1_offset),
+                IsOkAndHolds(EqualsProto(document1)));
+    // The document should be compressed since it's size is larger than the
+    // compression threshold.
+    ASSERT_LT(document1_size_written, document1_size);
+
+    ICING_ASSERT_OK_AND_ASSIGN(
+        auto document2_write_result,
+        WriteProtoAndReturnOffsetAndSize(proto_log.get(), document2));
+    document2_offset = document2_write_result.first;
+    int64_t document2_size_written = document2_write_result.second;
+    // Check that what we read is what we wrote
+    ASSERT_THAT(proto_log->ReadProto(document2_offset),
+                IsOkAndHolds(EqualsProto(document2)));
+    // The document should be uncompressed since it's size is smaller than the
+    // compression threshold.
+    ASSERT_GE(document2_size_written, document2_size);
+
+    ICING_ASSERT_OK(proto_log->PersistToDisk());
+  }
+
+  {
+    // Make a new proto_log with the same file_path, and make sure we can still
+    // read and write to the same underlying file.
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                /*compress_in=*/true, max_proto_size_,
+                /*compression_level_in=*/3,
+                /*compression_threshold_bytes_in=*/1000, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto recreated_proto_log = std::move(create_result.proto_log);
+    ASSERT_FALSE(create_result.has_data_loss());
+
+    // Check the first proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document1_offset),
+                IsOkAndHolds(EqualsProto(document1)));
+
+    // Check the second proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document2_offset),
+                IsOkAndHolds(EqualsProto(document2)));
+
+    // Write a third proto
+    DocumentProto document3 =
+        DocumentBuilder().SetKey("namespace3", "uri3").Build();
+    ICING_ASSERT_OK_AND_ASSIGN(int64_t document3_offset,
+                               recreated_proto_log->WriteProto(document3));
+    ASSERT_GT(document3_offset, document2_offset);
+    // Check the third proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document3_offset),
+                IsOkAndHolds(EqualsProto(document3)));
+  }
+}
+
+TEST_P(PortableFileBackedProtoLogTest, ChangingCompressionThresholdIsOk) {
+  // Create document1 with size of 1024 bytes.
+  int64_t document1_size = 1024;
+  int64_t document1_offset;
+  int64_t document1_size_written;
+  DocumentProto document1 =
+      DocumentBuilder()
+          .SetKey("namespace1", "uri1")
+          .AddStringProperty("long_str", std::string(990, 'a'))
+          .Build();
+  ASSERT_THAT(document1.ByteSizeLong(), Eq(document1_size));
+
+  // Create document2 with size of 900 bytes.
+  int64_t document2_size = 900;
+  int64_t document2_offset;
+  int64_t document2_size_written;
+  DocumentProto document2 =
+      DocumentBuilder()
+          .SetKey("namespace1", "uri1")
+          .AddStringProperty("long_str", std::string(866, 'a'))
+          .Build();
+  ASSERT_THAT(document2.ByteSizeLong(), Eq(document2_size));
+
+  {
+    // Create a proto_log with compression threshold of 1000 bytes
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                /*compress_in=*/true, max_proto_size_,
+                /*compression_level_in=*/3,
+                /*compression_threshold_bytes_in=*/1000, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto proto_log = std::move(create_result.proto_log);
+    ASSERT_FALSE(create_result.has_data_loss());
+
+    ICING_ASSERT_OK_AND_ASSIGN(
+        auto document1_write_result,
+        WriteProtoAndReturnOffsetAndSize(proto_log.get(), document1));
+    document1_offset = document1_write_result.first;
+    document1_size_written = document1_write_result.second;
+    // Check that what we read is what we wrote
+    ASSERT_THAT(proto_log->ReadProto(document1_offset),
+                IsOkAndHolds(EqualsProto(document1)));
+    // The document should be compressed since it's size is larger than the
+    // compression threshold.
+    ASSERT_LT(document1_size_written, document1_size);
+
+    ICING_ASSERT_OK_AND_ASSIGN(
+        auto document2_write_result,
+        WriteProtoAndReturnOffsetAndSize(proto_log.get(), document2));
+    document2_offset = document2_write_result.first;
+    document2_size_written = document2_write_result.second;
+    // Check that what we read is what we wrote
+    ASSERT_THAT(proto_log->ReadProto(document2_offset),
+                IsOkAndHolds(EqualsProto(document2)));
+    // The document should be uncompressed since it's size is smaller than the
+    // compression threshold.
+    ASSERT_GE(document2_size_written, document2_size);
+
+    ICING_ASSERT_OK(proto_log->PersistToDisk());
+  }
+
+  {
+    // Make a new proto_log with the same file_path with a different compression
+    // threshold, and make sure we can still read and write to the same
+    // underlying file.
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                /*compress_in=*/true, max_proto_size_,
+                /*compression_level_in=*/3,
+                /*compression_threshold_bytes_in=*/100, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto recreated_proto_log = std::move(create_result.proto_log);
+    ASSERT_FALSE(create_result.has_data_loss());
+
+    // Check the first proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document1_offset),
+                IsOkAndHolds(EqualsProto(document1)));
+
+    // Check the second proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document2_offset),
+                IsOkAndHolds(EqualsProto(document2)));
+
+    // Write document1 again as the third proto;
+    ICING_ASSERT_OK_AND_ASSIGN(
+        auto document3_write_result,
+        WriteProtoAndReturnOffsetAndSize(recreated_proto_log.get(), document1));
+    int64_t document3_offset = document3_write_result.first;
+    int64_t document3_size_written = document3_write_result.second;
+    // With this new compression threshold, document1 should still be
+    // compressed.
+    ASSERT_EQ(document3_size_written, document1_size_written);
+    ASSERT_GT(document3_offset, document2_offset);
+    // Check the third proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document3_offset),
+                IsOkAndHolds(EqualsProto(document1)));
+
+    // Write document2 again as the fourth proto;
+    ICING_ASSERT_OK_AND_ASSIGN(
+        auto document4_write_result,
+        WriteProtoAndReturnOffsetAndSize(recreated_proto_log.get(), document2));
+    int64_t document4_offset = document4_write_result.first;
+    int64_t document4_size_written = document4_write_result.second;
+    // With this new compression threshold, document2 should now be compressed.
+    ASSERT_LT(document4_size_written, document2_size_written);
+    ASSERT_LT(document4_size_written, document2_size);
+    ASSERT_GT(document4_offset, document3_offset);
+    // Check the fourth proto
+    ASSERT_THAT(recreated_proto_log->ReadProto(document4_offset),
+                IsOkAndHolds(EqualsProto(document2)));
+  }
+}
+
+TEST_P(PortableFileBackedProtoLogTest, CorruptedLegacyHeaderSection) {
+  // This test simulates the following scenario:
+  // - Log checksum matches.
+  // - Unsynced tail checksum matches.
+  // - Legacy header checksum doesn't match.
+  // - Header checksum matches.
+  //
+  // Expected behavior: full data loss.
   {
     ICING_ASSERT_OK_AND_ASSIGN(
         PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto recreated_proto_log = std::move(create_result.proto_log);
     EXPECT_FALSE(create_result.has_data_loss());
   }
 
-  int corrupt_checksum = 24;
+  int corrupt_legacy_header_checksum = 24;
 
-  // Write the corrupted header
-  Header header = ReadHeader(filesystem_, file_path_);
-  header.SetHeaderChecksum(corrupt_checksum);
-  WriteHeader(filesystem_, file_path_, header);
+  // Manually overwrite the legacy header checksum to simulate legacy header
+  // section is corrupted.
+  Header corrupted_header = ReadHeader(filesystem_, file_path_);
+  corrupted_header.SetLegacyHeaderChecksum(corrupt_legacy_header_checksum);
+  if (GetParam()) {
+    corrupted_header.SetHeaderChecksum(
+        corrupted_header.CalculateHeaderChecksum().Get());
+  }
+  WriteHeader(filesystem_, file_path_, corrupted_header);
 
   {
     // Reinitialize the same proto_log
     ASSERT_THAT(PortableFileBackedProtoLog<DocumentProto>::Create(
                     &filesystem_, file_path_,
                     PortableFileBackedProtoLog<DocumentProto>::Options(
-                        compress_, max_proto_size_, compression_level_)),
+                        compress_, max_proto_size_, compression_level_,
+                        compression_threshold_bytes_, compression_mem_level_,
+                        enable_smaller_decompression_buffer_size_,
+                        /*enable_new_header_format_in=*/GetParam(),
+                        /*enable_reusable_decompression_buffer=*/true)),
                 StatusIs(libtextclassifier3::StatusCode::INTERNAL,
-                         HasSubstr("Invalid header checksum")));
+                         HasSubstr("Invalid legacy header checksum")));
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest, DifferentMagic) {
+TEST_P(PortableFileBackedProtoLogTest, CorruptedHeaderChecksum) {
+  if (!GetParam()) {
+    GTEST_SKIP() << "This test is only relevant to the new header format.";
+  }
+
+  // This test simulates the following scenario:
+  // - Log checksum matches.
+  // - Unsynced tail checksum matches.
+  // - Legacy header checksum matches.
+  // - Header checksum doesn't match.
+  //
+  // Expected behavior:
+  // - Due to compatibility issue, PortableFileBackedProtoLog should assume that
+  //   the existing dataset is generated by an older version which does not have
+  //   the new header checksum logic.
+  // - Should only throw away the data in the unsynced tail section.
+  // - Related checksums should be recomputed and rewritten during
+  //   initialization.
+
+  DocumentProto document1 =
+      DocumentBuilder().SetKey("namespace", "uri1").Build();
+  DocumentProto document2 =
+      DocumentBuilder().SetKey("namespace", "uri2").Build();
+  DocumentProto document3 =
+      DocumentBuilder().SetKey("namespace", "uri3").Build();
+  DocumentProto document4 =
+      DocumentBuilder().SetKey("namespace", "uri4").Build();
+  DocumentProto document5 =
+      DocumentBuilder().SetKey("namespace", "uri5").Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+      PortableFileBackedProtoLog<DocumentProto>::Create(
+          &filesystem_, file_path_,
+          PortableFileBackedProtoLog<DocumentProto>::Options(
+              compress_, max_proto_size_, compression_level_,
+              compression_threshold_bytes_, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
+  auto proto_log = std::move(create_result.proto_log);
+  EXPECT_FALSE(create_result.has_data_loss());
+
+  // Write 3 protos to the log and call PersistToDisk to make them into log
+  // checksummed section. Record the header at this state.
+  ICING_ASSERT_OK(proto_log->WriteProto(document1));
+  ICING_ASSERT_OK(proto_log->WriteProto(document2));
+  ICING_ASSERT_OK(proto_log->WriteProto(document3));
+  ICING_ASSERT_OK(proto_log->PersistToDisk());
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Crc32(3230460271u)));
+
+  // Read the header for the state of doc1-to-doc3.
+  Header header_doc1_to_doc3 = ReadHeader(filesystem_, file_path_);
+  // Sanity check that the unsynced tail checksum is 0.
+  ASSERT_THAT(header_doc1_to_doc3.GetUnsyncedTailChecksum(), Eq(0));
+
+  // Write 2 more protos and keep them in the unsynced tail section (don't call
+  // PersistToDisk, UpdateChecksum, or destructor).
+  ICING_ASSERT_OK(proto_log->WriteProto(document4));
+  ICING_ASSERT_OK(proto_log->WriteProto(document5));
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Crc32(705827520u)));
+
+  // Read the header for the state of doc1-to-doc5.
+  Header header_doc1_to_doc5 = ReadHeader(filesystem_, file_path_);
+  // Sanity check:
+  // - The legacy and log checksums are same as doc1-to-doc3.
+  // - The unsynced tail checksum and header checksum are different from state
+  //   doc1-to-doc3.
+  ASSERT_THAT(header_doc1_to_doc5.GetLegacyHeaderChecksum(),
+              Eq(header_doc1_to_doc3.GetLegacyHeaderChecksum()));
+  ASSERT_THAT(header_doc1_to_doc5.GetLogChecksum(),
+              Eq(header_doc1_to_doc3.GetLogChecksum()));
+  ASSERT_THAT(header_doc1_to_doc5.GetUnsyncedTailChecksum(),
+              Ne(header_doc1_to_doc3.GetUnsyncedTailChecksum()));
+  ASSERT_THAT(header_doc1_to_doc5.GetHeaderChecksum(),
+              Ne(header_doc1_to_doc3.GetHeaderChecksum()));
+
+  // Manually overwrite the header checksum.
+  Header corrupted_header = header_doc1_to_doc5;
+  corrupted_header.SetHeaderChecksum(corrupted_header.GetHeaderChecksum() +
+                                     12345);
+  WriteHeader(filesystem_, file_path_, corrupted_header);
+
+  // Initialize another instance with the same proto file. This should only
+  // trigger partial data loss. IOW, we only throw away the data in the unsynced
+  // tail section, so the data state is reverted back to doc1-to-doc3.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result2,
+      PortableFileBackedProtoLog<DocumentProto>::Create(
+          &filesystem_, file_path_,
+          PortableFileBackedProtoLog<DocumentProto>::Options(
+              compress_, max_proto_size_, compression_level_,
+              compression_threshold_bytes_, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
+  EXPECT_THAT(create_result2.data_loss, Eq(DataLoss::PARTIAL));
+
+  // Check the overall checksum.
+  EXPECT_THAT(create_result2.proto_log->GetChecksum(),
+              IsOkAndHolds(Crc32(3230460271u)));
+
+  // Manually check the header.
+  Header header_after = ReadHeader(filesystem_, file_path_);
+  EXPECT_THAT(header_after.GetUnsyncedTailChecksum(),
+              Eq(header_doc1_to_doc3.GetUnsyncedTailChecksum()));
+  EXPECT_THAT(header_after.GetHeaderChecksum(),
+              Eq(header_doc1_to_doc3.GetHeaderChecksum()));
+}
+
+TEST_P(PortableFileBackedProtoLogTest, CorruptedUnsyncedTailChecksum) {
+  if (!GetParam()) {
+    GTEST_SKIP() << "This test is only relevant to the new header format.";
+  }
+
+  // This test simulates the following scenario:
+  // - Log checksum matches.
+  // - Unsynced tail checksum doesn't match.
+  // - Legacy header checksum matches.
+  // - Header checksum matches.
+  //
+  // Expected behavior:
+  // - Should only throw away the data in the unsynced tail section.
+  // - Related checksums should be recomputed and rewritten during
+  //   initialization.
+
+  DocumentProto document1 =
+      DocumentBuilder().SetKey("namespace", "uri1").Build();
+  DocumentProto document2 =
+      DocumentBuilder().SetKey("namespace", "uri2").Build();
+  DocumentProto document3 =
+      DocumentBuilder().SetKey("namespace", "uri3").Build();
+  DocumentProto document4 =
+      DocumentBuilder().SetKey("namespace", "uri4").Build();
+  DocumentProto document5 =
+      DocumentBuilder().SetKey("namespace", "uri5").Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+      PortableFileBackedProtoLog<DocumentProto>::Create(
+          &filesystem_, file_path_,
+          PortableFileBackedProtoLog<DocumentProto>::Options(
+              compress_, max_proto_size_, compression_level_,
+              compression_threshold_bytes_, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
+  auto proto_log = std::move(create_result.proto_log);
+  EXPECT_FALSE(create_result.has_data_loss());
+
+  // Write 3 protos to the log and call PersistToDisk to make them into log
+  // checksummed section. Record the header at this state.
+  ICING_ASSERT_OK(proto_log->WriteProto(document1));
+  ICING_ASSERT_OK(proto_log->WriteProto(document2));
+  ICING_ASSERT_OK(proto_log->WriteProto(document3));
+  ICING_ASSERT_OK(proto_log->PersistToDisk());
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Crc32(3230460271u)));
+
+  // Read the header for the state of doc1-to-doc3.
+  Header header_doc1_to_doc3 = ReadHeader(filesystem_, file_path_);
+  // Sanity check that the unsynced tail checksum is 0.
+  ASSERT_THAT(header_doc1_to_doc3.GetUnsyncedTailChecksum(), Eq(0));
+
+  // Write 2 more protos and keep them in the unsynced tail section (don't call
+  // PersistToDisk, UpdateChecksum, or destructor).
+  ICING_ASSERT_OK(proto_log->WriteProto(document4));
+  ICING_ASSERT_OK(proto_log->WriteProto(document5));
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Crc32(705827520u)));
+
+  // Read the header for the state of doc1-to-doc5.
+  Header header_doc1_to_doc5 = ReadHeader(filesystem_, file_path_);
+  // Sanity check that the unsynced tail checksum and header checksum are
+  // different from state doc1-to-doc3.
+  ASSERT_THAT(header_doc1_to_doc5.GetUnsyncedTailChecksum(),
+              Ne(header_doc1_to_doc3.GetUnsyncedTailChecksum()));
+  ASSERT_THAT(header_doc1_to_doc5.GetHeaderChecksum(),
+              Ne(header_doc1_to_doc3.GetHeaderChecksum()));
+
+  // Manually overwrite the unsynced tail checksum and recompute header
+  // checksum.
+  Header corrupted_header = header_doc1_to_doc5;
+  corrupted_header.SetUnsyncedTailChecksum(
+      corrupted_header.GetUnsyncedTailChecksum() + 12345);
+  corrupted_header.SetHeaderChecksum(
+      corrupted_header.CalculateHeaderChecksum().Get());
+  WriteHeader(filesystem_, file_path_, corrupted_header);
+
+  // Initialize another instance with the same proto file. This should only
+  // trigger partial data loss. IOW, we only throw away the data in the unsynced
+  // tail section, so the data state is reverted back to doc1-to-doc3.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result2,
+      PortableFileBackedProtoLog<DocumentProto>::Create(
+          &filesystem_, file_path_,
+          PortableFileBackedProtoLog<DocumentProto>::Options(
+              compress_, max_proto_size_, compression_level_,
+              compression_threshold_bytes_, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
+  EXPECT_THAT(create_result2.data_loss, Eq(DataLoss::PARTIAL));
+
+  // Check the overall checksum.
+  EXPECT_THAT(create_result2.proto_log->GetChecksum(),
+              IsOkAndHolds(Crc32(3230460271u)));
+
+  // Manually check the header.
+  Header header_after = ReadHeader(filesystem_, file_path_);
+  EXPECT_THAT(header_after.GetUnsyncedTailChecksum(),
+              Eq(header_doc1_to_doc3.GetUnsyncedTailChecksum()));
+  EXPECT_THAT(header_after.GetHeaderChecksum(),
+              Eq(header_doc1_to_doc3.GetHeaderChecksum()));
+}
+
+TEST_P(PortableFileBackedProtoLogTest, DifferentMagic) {
   {
     ICING_ASSERT_OK_AND_ASSIGN(
         PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto recreated_proto_log = std::move(create_result.proto_log);
     EXPECT_FALSE(create_result.has_data_loss());
 
@@ -547,16 +1394,22 @@
 
   {
     // Reinitialize the same proto_log
-    ASSERT_THAT(PortableFileBackedProtoLog<DocumentProto>::Create(
-                    &filesystem_, file_path_,
-                    PortableFileBackedProtoLog<DocumentProto>::Options(
-                        compress_, max_proto_size_, compression_level_)),
-                StatusIs(libtextclassifier3::StatusCode::INTERNAL,
-                         HasSubstr("Invalid header kMagic")));
+    ASSERT_THAT(
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)),
+        StatusIs(
+            libtextclassifier3::StatusCode::INTERNAL,
+            HasSubstr("Invalid header magic for PortableFileBackedProtoLog")));
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest,
+TEST_P(PortableFileBackedProtoLogTest,
        UnableToDetectCorruptContentWithoutDirtyBit) {
   // This is intentional that we can't detect corruption. We're trading off
   // earlier corruption detection for lower initialization latency. By not
@@ -573,7 +1426,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     EXPECT_FALSE(create_result.has_data_loss());
 
@@ -600,7 +1457,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     EXPECT_FALSE(create_result.has_data_loss());
     EXPECT_THAT(create_result.data_loss, Eq(DataLoss::NONE));
@@ -614,7 +1475,7 @@
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest,
+TEST_P(PortableFileBackedProtoLogTest,
        DetectAndThrowOutCorruptContentWithDirtyBit) {
   {
     ICING_ASSERT_OK_AND_ASSIGN(
@@ -622,7 +1483,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -657,7 +1522,7 @@
 
     // Set dirty bit to true to reflect that something changed in the log.
     header.SetDirtyFlag(true);
-    header.SetHeaderChecksum(header.CalculateHeaderChecksum());
+    header.UpdateHeaderChecksums(/*enable_new_header_format=*/GetParam());
 
     WriteHeader(filesystem_, file_path_, header);
   }
@@ -668,7 +1533,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     EXPECT_TRUE(create_result.has_data_loss());
     EXPECT_THAT(create_result.data_loss, Eq(DataLoss::COMPLETE));
@@ -687,7 +1556,7 @@
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest, DirtyBitFalseAlarmKeepsData) {
+TEST_P(PortableFileBackedProtoLogTest, DirtyBitFalseAlarmKeepsData) {
   DocumentProto document =
       DocumentBuilder().SetKey("namespace1", "uri1").Build();
   int64_t document_offset;
@@ -697,7 +1566,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -716,7 +1589,7 @@
     // Simulate the dirty flag set as true, but no data has been changed yet.
     // Maybe we crashed between writing the dirty flag and erasing a proto.
     header.SetDirtyFlag(true);
-    header.SetHeaderChecksum(header.CalculateHeaderChecksum());
+    header.UpdateHeaderChecksums(/*enable_new_header_format=*/GetParam());
 
     WriteHeader(filesystem_, file_path_, header);
   }
@@ -727,7 +1600,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     EXPECT_FALSE(create_result.has_data_loss());
 
@@ -744,7 +1621,7 @@
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest,
+TEST_P(PortableFileBackedProtoLogTest,
        PersistToDiskKeepsPersistedDataAndTruncatesExtraData) {
   DocumentProto document1 =
       DocumentBuilder().SetKey("namespace1", "uri1").Build();
@@ -759,7 +1636,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -805,7 +1686,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_TRUE(create_result.has_data_loss());
     ASSERT_THAT(create_result.data_loss, Eq(DataLoss::PARTIAL));
@@ -822,7 +1707,83 @@
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest,
+TEST_P(PortableFileBackedProtoLogTest, UnsyncedTailChecksumKeepsData) {
+  if (!GetParam()) {
+    GTEST_SKIP() << "This test is only relevant to the new header format.";
+  }
+
+  DocumentProto document1 =
+      DocumentBuilder().SetKey("namespace", "uri1").Build();
+  DocumentProto document2 =
+      DocumentBuilder().SetKey("namespace", "uri2").Build();
+  DocumentProto document3 =
+      DocumentBuilder().SetKey("namespace", "uri3").Build();
+  DocumentProto document4 =
+      DocumentBuilder().SetKey("namespace", "uri4").Build();
+  DocumentProto document5 =
+      DocumentBuilder().SetKey("namespace", "uri5").Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+      PortableFileBackedProtoLog<DocumentProto>::Create(
+          &filesystem_, file_path_,
+          PortableFileBackedProtoLog<DocumentProto>::Options(
+              compress_, max_proto_size_, compression_level_,
+              compression_threshold_bytes_, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
+  auto proto_log = std::move(create_result.proto_log);
+  EXPECT_FALSE(create_result.has_data_loss());
+
+  // Write 3 protos to the log and call PersistToDisk to make them into log
+  // checksummed section. Record the header at this state.
+  ICING_ASSERT_OK(proto_log->WriteProto(document1));
+  ICING_ASSERT_OK(proto_log->WriteProto(document2));
+  ICING_ASSERT_OK(proto_log->WriteProto(document3));
+  ICING_ASSERT_OK(proto_log->PersistToDisk());
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Crc32(3230460271u)));
+
+  // Write 2 more protos and keep them in the unsynced tail section (don't call
+  // PersistToDisk, UpdateChecksum, or destructor).
+  ICING_ASSERT_OK(proto_log->WriteProto(document4));
+  ICING_ASSERT_OK(proto_log->WriteProto(document5));
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Crc32(705827520u)));
+
+  // Read the header for the state of doc1-to-doc5.
+  Header header_doc1_to_doc5 = ReadHeader(filesystem_, file_path_);
+
+  // Initialize another instance with the same proto file. Thanks to the
+  // unsynced tail checksum, document4 and document5 should be kept.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result2,
+      PortableFileBackedProtoLog<DocumentProto>::Create(
+          &filesystem_, file_path_,
+          PortableFileBackedProtoLog<DocumentProto>::Options(
+              compress_, max_proto_size_, compression_level_,
+              compression_threshold_bytes_, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
+  EXPECT_FALSE(create_result2.has_data_loss());
+
+  // Check the overall checksum. Should remain the same.
+  EXPECT_THAT(create_result2.proto_log->GetChecksum(),
+              IsOkAndHolds(Crc32(705827520u)));
+
+  // Manually check the header. Should remain the same.
+  Header header_after = ReadHeader(filesystem_, file_path_);
+  EXPECT_THAT(header_after.GetLegacyHeaderChecksum(),
+              Eq(header_doc1_to_doc5.GetLegacyHeaderChecksum()));
+  EXPECT_THAT(header_after.GetRewindOffset(),
+              Eq(header_doc1_to_doc5.GetRewindOffset()));
+  EXPECT_THAT(header_after.GetUnsyncedTailChecksum(),
+              Eq(header_doc1_to_doc5.GetUnsyncedTailChecksum()));
+  EXPECT_THAT(header_after.GetHeaderChecksum(),
+              Eq(header_doc1_to_doc5.GetHeaderChecksum()));
+}
+
+TEST_P(PortableFileBackedProtoLogTest,
        DirtyBitIsFalseAfterPutAndPersistToDisk) {
   {
     ICING_ASSERT_OK_AND_ASSIGN(
@@ -830,7 +1791,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -853,7 +1818,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
 
     // We previously persisted to disk so everything should be in a perfect
     // state.
@@ -865,7 +1834,7 @@
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest,
+TEST_P(PortableFileBackedProtoLogTest,
        DirtyBitIsFalseAfterDeleteAndPersistToDisk) {
   {
     ICING_ASSERT_OK_AND_ASSIGN(
@@ -873,7 +1842,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -897,7 +1870,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
 
     // We previously persisted to disk so everything should be in a perfect
     // state.
@@ -909,14 +1886,18 @@
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest, DirtyBitIsFalseAfterPutAndDestructor) {
+TEST_P(PortableFileBackedProtoLogTest, DirtyBitIsFalseAfterPutAndDestructor) {
   {
     ICING_ASSERT_OK_AND_ASSIGN(
         PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -941,7 +1922,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
 
     // We previously persisted to disk so everything should be in a perfect
     // state.
@@ -953,7 +1938,7 @@
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest,
+TEST_P(PortableFileBackedProtoLogTest,
        DirtyBitIsFalseAfterDeleteAndDestructor) {
   {
     ICING_ASSERT_OK_AND_ASSIGN(
@@ -961,7 +1946,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -987,7 +1976,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
 
     // We previously persisted to disk so everything should be in a perfect
     // state.
@@ -999,7 +1992,7 @@
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest, Iterator) {
+TEST_P(PortableFileBackedProtoLogTest, Iterator) {
   DocumentProto document1 =
       DocumentBuilder().SetKey("namespace", "uri1").Build();
   DocumentProto document2 =
@@ -1010,7 +2003,11 @@
       PortableFileBackedProtoLog<DocumentProto>::Create(
           &filesystem_, file_path_,
           PortableFileBackedProtoLog<DocumentProto>::Options(
-              compress_, max_proto_size_, compression_level_)));
+              compress_, max_proto_size_, compression_level_,
+              compression_threshold_bytes_, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
   auto proto_log = std::move(create_result.proto_log);
   ASSERT_FALSE(create_result.has_data_loss());
 
@@ -1040,7 +2037,7 @@
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest, UpdateChecksum) {
+TEST_P(PortableFileBackedProtoLogTest, UpdateChecksum) {
   DocumentProto document = DocumentBuilder().SetKey("namespace", "uri").Build();
   Crc32 checksum;
 
@@ -1050,7 +2047,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -1070,7 +2071,11 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
@@ -1091,7 +2096,7 @@
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest, EraseProtoShouldSetZero) {
+TEST_P(PortableFileBackedProtoLogTest, EraseProtoShouldSetZero) {
   DocumentProto document1 =
       DocumentBuilder().SetKey("namespace", "uri1").Build();
 
@@ -1100,7 +2105,11 @@
       PortableFileBackedProtoLog<DocumentProto>::Create(
           &filesystem_, file_path_,
           PortableFileBackedProtoLog<DocumentProto>::Options(
-              compress_, max_proto_size_, compression_level_)));
+              compress_, max_proto_size_, compression_level_,
+              compression_threshold_bytes_, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
   auto proto_log = std::move(create_result.proto_log);
   ASSERT_FALSE(create_result.has_data_loss());
 
@@ -1125,7 +2134,7 @@
   }
 }
 
-TEST_F(PortableFileBackedProtoLogTest, EraseProtoShouldReturnNotFound) {
+TEST_P(PortableFileBackedProtoLogTest, EraseProtoShouldReturnNotFound) {
   DocumentProto document1 =
       DocumentBuilder().SetKey("namespace", "uri1").Build();
   DocumentProto document2 =
@@ -1136,7 +2145,11 @@
       PortableFileBackedProtoLog<DocumentProto>::Create(
           &filesystem_, file_path_,
           PortableFileBackedProtoLog<DocumentProto>::Options(
-              compress_, max_proto_size_, compression_level_)));
+              compress_, max_proto_size_, compression_level_,
+              compression_threshold_bytes_, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
   auto proto_log = std::move(create_result.proto_log);
   ASSERT_FALSE(create_result.has_data_loss());
 
@@ -1157,7 +2170,201 @@
               IsOkAndHolds(EqualsProto(document2)));
 }
 
-TEST_F(PortableFileBackedProtoLogTest, ChecksumShouldBeCorrectWithErasedProto) {
+TEST_P(PortableFileBackedProtoLogTest, GetChecksumShouldNotUpdateHeader) {
+  // This test verifies that GetChecksum() returns the correct checksum after
+  // each change, but the header checksum is not updated if UpdateChecksum() or
+  // PersistToDisk() is not called.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+      PortableFileBackedProtoLog<DocumentProto>::Create(
+          &filesystem_, file_path_,
+          PortableFileBackedProtoLog<DocumentProto>::Options(
+              compress_, max_proto_size_, compression_level_,
+              compression_threshold_bytes_, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
+  auto proto_log = std::move(create_result.proto_log);
+  ASSERT_FALSE(create_result.has_data_loss());
+
+  // Sanity check: empty checksum.
+  ASSERT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(0u))));
+
+  // Put document1 and persist to disk.
+  DocumentProto document1 =
+      DocumentBuilder().SetKey("namespace", "uri1").Build();
+  ICING_ASSERT_OK(proto_log->WriteProto(document1));
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(1883553267u))));
+  ICING_ASSERT_OK(proto_log->PersistToDisk());
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(1883553267u))));
+
+  // Put document2 without calling UpdateChecksum() or PersistToDisk().
+  DocumentProto document2 =
+      DocumentBuilder().SetKey("namespace", "uri2").Build();
+  ICING_ASSERT_OK(proto_log->WriteProto(document2));
+
+  // Read the header.
+  Header header_before_get_checksum = ReadHeader(filesystem_, file_path_);
+  EXPECT_THAT(header_before_get_checksum.GetLegacyHeaderChecksum(),
+              Eq(1460433589u));
+  EXPECT_THAT(header_before_get_checksum.GetLogChecksum(), Eq(1883553267u));
+  CheckNewChecksums(header_before_get_checksum,
+                    /*expected_unsynced_tail_checksum=*/3503817807u,
+                    /*expected_header_checksum=*/1080094584u,
+                    /*enable_new_header_format=*/GetParam());
+
+  // Since there is a new document (document2) in the unsynced tail, the file is
+  // dirty. GetChecksum():
+  // - Should return the checksum reflecting the new written proto (document2).
+  // - Should not update anything in the header.
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(1632103647u))));
+
+  // Manually check the header: although GetChecksum() reflects the latest
+  // changes, the header file should remain unchanged.
+  Header header_after_get_checksum = ReadHeader(filesystem_, file_path_);
+  EXPECT_THAT(header_after_get_checksum.GetLegacyHeaderChecksum(),
+              Eq(1460433589u));
+  EXPECT_THAT(header_after_get_checksum.GetLogChecksum(), Eq(1883553267u));
+  CheckNewChecksums(header_after_get_checksum,
+                    /*expected_unsynced_tail_checksum=*/3503817807u,
+                    /*expected_header_checksum=*/1080094584u,
+                    /*enable_new_header_format=*/GetParam());
+  EXPECT_THAT(header_after_get_checksum.GetRewindOffset(),
+              Eq(header_before_get_checksum.GetRewindOffset()));
+
+  // Call UpdateChecksum(). Should update the header.
+  // - Legacy header checksum should be updated.
+  // - Log checksum should be updated.
+  // - Rewind offset should be updated.
+  ICING_ASSERT_OK(proto_log->UpdateChecksum());
+  Header header_after_update_checksum = ReadHeader(filesystem_, file_path_);
+  EXPECT_THAT(header_after_update_checksum.GetLegacyHeaderChecksum(),
+              Eq(455544443u));
+  EXPECT_THAT(header_after_update_checksum.GetLogChecksum(), Eq(1632103647u));
+  // If the flag is enabled, the unsynced tail checksum and header checksum
+  // should be updated. Since we merged unsynced tail section into log
+  // checksummed section, the unsynced tail checksum should be updated to 0.
+  CheckNewChecksums(header_after_update_checksum,
+                    /*expected_unsynced_tail_checksum=*/0u,
+                    /*expected_header_checksum=*/2472053009u,
+                    /*enable_new_header_format=*/GetParam());
+  EXPECT_THAT(header_after_update_checksum.GetRewindOffset(),
+              Ne(header_after_get_checksum.GetRewindOffset()));
+}
+
+TEST_P(PortableFileBackedProtoLogTest,
+       GetChecksumShouldBeCorrectWithWriteProto) {
+  // This test verifies that GetChecksum() returns the correct checksum after
+  // each change, even if UpdateChecksum() or PersistToDisk() is not called.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+      PortableFileBackedProtoLog<DocumentProto>::Create(
+          &filesystem_, file_path_,
+          PortableFileBackedProtoLog<DocumentProto>::Options(
+              compress_, max_proto_size_, compression_level_,
+              compression_threshold_bytes_, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
+  auto proto_log = std::move(create_result.proto_log);
+  ASSERT_FALSE(create_result.has_data_loss());
+
+  DocumentProto document1 =
+      DocumentBuilder().SetKey("namespace", "uri1").Build();
+  ICING_ASSERT_OK(proto_log->WriteProto(document1));
+  // Although we didn't call UpdateChecksum() or PersistToDisk(), GetChecksum()
+  // should return the value reflecting the new written proto (document1).
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(1883553267u))));
+
+  DocumentProto document2 =
+      DocumentBuilder().SetKey("namespace", "uri2").Build();
+  ICING_ASSERT_OK(proto_log->WriteProto(document2));
+
+  const Crc32 kChecksumWithDocument2(1632103647u);
+
+  // Although we didn't call UpdateChecksum() or PersistToDisk(), GetChecksum()
+  // should return the value reflecting the new written proto (document1,
+  // document2).
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(kChecksumWithDocument2));
+
+  // Call PersistToDisk.
+  ICING_ASSERT_OK(proto_log->PersistToDisk());
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(kChecksumWithDocument2));
+
+  // Read the header.
+  Header header_before_write_proto = ReadHeader(filesystem_, file_path_);
+
+  // Put document3 without calling UpdateChecksum() or PersistToDisk().
+  DocumentProto document3 =
+      DocumentBuilder().SetKey("namespace", "uri3").Build();
+  ICING_ASSERT_OK(proto_log->WriteProto(document3));
+
+  const Crc32 kChecksumWithDocument3(3230460271u);
+  // Although we didn't call UpdateChecksum() or PersistToDisk(), GetChecksum()
+  // should return the value reflecting the new written proto (document3).
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(kChecksumWithDocument3));
+
+  // Manually check the header.
+  Header header_after_write_proto = ReadHeader(filesystem_, file_path_);
+  EXPECT_THAT(header_after_write_proto.GetLegacyHeaderChecksum(),
+              Eq(header_before_write_proto.GetLegacyHeaderChecksum()));
+  EXPECT_THAT(header_after_write_proto.GetRewindOffset(),
+              Eq(header_before_write_proto.GetRewindOffset()));
+  EXPECT_THAT(header_after_write_proto.GetLogChecksum(),
+              Eq(header_before_write_proto.GetLogChecksum()));
+  if (GetParam()) {
+    // If the flag is enabled, when writing document3, the unsynced tail
+    // checksum and header checksum should be updated.
+    EXPECT_THAT(header_after_write_proto.GetUnsyncedTailChecksum(),
+                Ne(header_before_write_proto.GetUnsyncedTailChecksum()));
+    EXPECT_THAT(header_after_write_proto.GetHeaderChecksum(),
+                Ne(header_before_write_proto.GetHeaderChecksum()));
+
+    // Create another instance of the same proto log.
+    // - Nothing will be discarded thanks to the unsynced tail checksum.
+    // - GetChecksum() should return the same checksum after writing document3.
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result2,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto proto_log2 = std::move(create_result2.proto_log);
+    EXPECT_FALSE(create_result2.has_data_loss());
+    EXPECT_THAT(proto_log2->GetChecksum(),
+                IsOkAndHolds(kChecksumWithDocument3));
+  } else {
+    // If the flag is disabled, the unsynced tail checksum and header checksum
+    // should remain 0.
+    EXPECT_THAT(header_after_write_proto.GetUnsyncedTailChecksum(), Eq(0u));
+    EXPECT_THAT(header_after_write_proto.GetHeaderChecksum(), Eq(0u));
+
+    // Create another instance of the same proto log.
+    // - document3 will be discarded since it is after the rewind offset.
+    // - GetChecksum() will return the old one before writing document3.
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result2,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto proto_log2 = std::move(create_result2.proto_log);
+    EXPECT_THAT(create_result2.data_loss, Eq(DataLoss::PARTIAL));
+    EXPECT_THAT(proto_log2->GetChecksum(),
+                IsOkAndHolds(kChecksumWithDocument2));
+  }
+}
+
+TEST_P(PortableFileBackedProtoLogTest,
+       GetChecksumShouldBeCorrectWithErasedProtoAfterRewindOffset) {
   DocumentProto document1 =
       DocumentBuilder().SetKey("namespace", "uri1").Build();
   DocumentProto document2 =
@@ -1166,80 +2373,69 @@
       DocumentBuilder().SetKey("namespace", "uri3").Build();
   DocumentProto document4 =
       DocumentBuilder().SetKey("namespace", "uri4").Build();
-
-  int64_t document2_offset;
-  int64_t document3_offset;
+  DocumentProto document5 =
+      DocumentBuilder().SetKey("namespace", "uri5").Build();
+  DocumentProto document6 =
+      DocumentBuilder().SetKey("namespace", "uri6").Build();
 
   {
-    // Erase data after the rewind position. This won't update the checksum
-    // immediately.
+    // Write 3 protos.
     ICING_ASSERT_OK_AND_ASSIGN(
         PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                /*compression_threshold_bytes_in=*/0, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
+    // Sanity check: empty checksum.
+    ASSERT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(0u))));
+
     // Writes 3 protos
-    ICING_ASSERT_OK_AND_ASSIGN(int64_t document1_offset,
-                               proto_log->WriteProto(document1));
-    ICING_ASSERT_OK_AND_ASSIGN(document2_offset,
-                               proto_log->WriteProto(document2));
-    ICING_ASSERT_OK_AND_ASSIGN(document3_offset,
-                               proto_log->WriteProto(document3));
-
-    // Erases the 1st proto, checksum won't be updated immediately because the
-    // rewind position is 0.
-    ICING_ASSERT_OK(proto_log->EraseProto(document1_offset));
-
-    EXPECT_THAT(proto_log->UpdateChecksum(),
-                IsOkAndHolds(Eq(Crc32(2175574628))));
-  }  // New checksum is updated in destructor.
+    ICING_ASSERT_OK(proto_log->WriteProto(document1));
+    ICING_ASSERT_OK(proto_log->WriteProto(document2));
+    ICING_ASSERT_OK(proto_log->WriteProto(document3));
+    // Although we didn't call UpdateChecksum() or PersistToDisk(),
+    // GetChecksum() should return the value reflecting the new written proto.
+    EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(3230460271u))));
+  }  // New checksum is written to the header and flushed in destructor.
 
   {
-    // Erase data before the rewind position. This will update the checksum
-    // immediately.
+    // Erase data after the rewind position.
     ICING_ASSERT_OK_AND_ASSIGN(
         PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                /*compression_threshold_bytes_in=*/0, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     ASSERT_FALSE(create_result.has_data_loss());
 
-    // Erases the 2nd proto that is now before the rewind position. Checksum
-    // is updated.
-    ICING_ASSERT_OK(proto_log->EraseProto(document2_offset));
+    // Sanity check that the checksum is identical to the previous one.
+    ASSERT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(3230460271u))));
 
-    EXPECT_THAT(proto_log->UpdateChecksum(),
-                IsOkAndHolds(Eq(Crc32(790877774))));
-  }
+    // Write 2 more documents to the unsynced tail.
+    ICING_ASSERT_OK_AND_ASSIGN(int64_t document4_offset,
+                               proto_log->WriteProto(document4));
+    ICING_ASSERT_OK(proto_log->WriteProto(document5));
+    // Although we didn't call UpdateChecksum() or PersistToDisk(),
+    // GetChecksum() should return the value reflecting the new written proto.
+    EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(705827520u))));
 
-  {
-    // Append data and erase data before the rewind position. This will update
-    // the checksum twice: in EraseProto() and destructor.
-    ICING_ASSERT_OK_AND_ASSIGN(
-        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
-        PortableFileBackedProtoLog<DocumentProto>::Create(
-            &filesystem_, file_path_,
-            PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
-    auto proto_log = std::move(create_result.proto_log);
-    ASSERT_FALSE(create_result.has_data_loss());
-
-    // Append a new document which is after the rewind position.
-    ICING_ASSERT_OK(proto_log->WriteProto(document4));
-
-    // Erases the 3rd proto that is now before the rewind position. Checksum
-    // is updated.
-    ICING_ASSERT_OK(proto_log->EraseProto(document3_offset));
-
-    EXPECT_THAT(proto_log->UpdateChecksum(),
-                IsOkAndHolds(Eq(Crc32(2344803210))));
-  }  // Checksum is updated with the newly appended document.
+    // Erases the 4th proto that is after the rewind position. GetChecksum()
+    // should return the new checksum reflecting the erased proto.
+    ICING_ASSERT_OK(proto_log->EraseProto(document4_offset));
+    EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(1052805596u))));
+  }  // New checksum is written to the header and flushed in destructor.
 
   {
     // A successful creation means that the checksum matches.
@@ -1248,12 +2444,419 @@
         PortableFileBackedProtoLog<DocumentProto>::Create(
             &filesystem_, file_path_,
             PortableFileBackedProtoLog<DocumentProto>::Options(
-                compress_, max_proto_size_, compression_level_)));
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
     auto proto_log = std::move(create_result.proto_log);
     EXPECT_FALSE(create_result.has_data_loss());
+
+    // The checksum should match the one from the previous case.
+    EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(1052805596u))));
   }
 }
 
+TEST_P(PortableFileBackedProtoLogTest,
+       GetChecksumShouldBeCorrectWithErasedProtoBeforeRewindOffset) {
+  DocumentProto document1 =
+      DocumentBuilder().SetKey("namespace", "uri1").Build();
+  DocumentProto document2 =
+      DocumentBuilder().SetKey("namespace", "uri2").Build();
+  DocumentProto document3 =
+      DocumentBuilder().SetKey("namespace", "uri3").Build();
+  DocumentProto document4 =
+      DocumentBuilder().SetKey("namespace", "uri4").Build();
+  DocumentProto document5 =
+      DocumentBuilder().SetKey("namespace", "uri5").Build();
+  DocumentProto document6 =
+      DocumentBuilder().SetKey("namespace", "uri6").Build();
+
+  int64_t document2_offset;
+  int64_t document3_offset;
+  {
+    // Write 3 protos.
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                compress_, max_proto_size_, compression_level_,
+                /*compression_threshold_bytes_in=*/0, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto proto_log = std::move(create_result.proto_log);
+    ASSERT_FALSE(create_result.has_data_loss());
+
+    // Sanity check: empty checksum.
+    ASSERT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(0u))));
+
+    // Writes 3 protos
+    ICING_ASSERT_OK(proto_log->WriteProto(document1));
+    ICING_ASSERT_OK_AND_ASSIGN(document2_offset,
+                               proto_log->WriteProto(document2));
+    ICING_ASSERT_OK_AND_ASSIGN(document3_offset,
+                               proto_log->WriteProto(document3));
+    // Although we didn't call UpdateChecksum() or PersistToDisk(),
+    // GetChecksum() should return the value reflecting the new written proto.
+    EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(3230460271u))));
+
+    // Call PersistToDisk() so that the unsynced tail is flushed and becomes
+    // synced section.
+    ICING_ASSERT_OK(proto_log->PersistToDisk());
+  }
+
+  {
+    // Erase data before the rewind position.
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                compress_, max_proto_size_, compression_level_,
+                /*compression_threshold_bytes_in=*/0, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto proto_log = std::move(create_result.proto_log);
+    ASSERT_FALSE(create_result.has_data_loss());
+
+    // Sanity check that the checksum is identical to the previous one.
+    ASSERT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(3230460271u))));
+
+    // Erases the 2nd proto that is before the rewind position.
+    // GetChecksum() should return the new checksum reflecting the erased proto.
+    ICING_ASSERT_OK(proto_log->EraseProto(document2_offset));
+    EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(1845730629u))));
+  }  // New checksum is written to the header and flushed in destructor.
+
+  {
+    // Append data to the unsynced tail and erase data before the rewind
+    // position.
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                compress_, max_proto_size_, compression_level_,
+                /*compression_threshold_bytes_in=*/0, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto proto_log = std::move(create_result.proto_log);
+    ASSERT_FALSE(create_result.has_data_loss());
+
+    // Sanity check that the checksum is identical to the previous one.
+    ASSERT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(1845730629u))));
+
+    // Append a new document which is after the rewind position.
+    ICING_ASSERT_OK(proto_log->WriteProto(document6));
+
+    // Erases the 3rd proto that is before the rewind position.
+    // GetChecksum() should return the new checksum reflecting document6 and the
+    // erased proto.
+    ICING_ASSERT_OK(proto_log->EraseProto(document3_offset));
+    EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(3659111045u))));
+  }  // New checksum is written to the header and flushed in destructor.
+
+  {
+    // A successful creation means that the checksum matches.
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                compress_, max_proto_size_, compression_level_,
+                compression_threshold_bytes_, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto proto_log = std::move(create_result.proto_log);
+    EXPECT_FALSE(create_result.has_data_loss());
+
+    // The checksum should match the one from the previous case.
+    EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(3659111045u))));
+  }
+}
+
+TEST_P(PortableFileBackedProtoLogTest, ErasedProtoAfterRewindOffset) {
+  DocumentProto document1 =
+      DocumentBuilder().SetKey("namespace", "uri1").Build();
+  DocumentProto document2 =
+      DocumentBuilder().SetKey("namespace", "uri2").Build();
+  DocumentProto document3 =
+      DocumentBuilder().SetKey("namespace", "uri3").Build();
+  DocumentProto document4 =
+      DocumentBuilder().SetKey("namespace", "uri4").Build();
+  DocumentProto document5 =
+      DocumentBuilder().SetKey("namespace", "uri5").Build();
+
+  {
+    // Write 3 protos.
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                compress_, max_proto_size_, compression_level_,
+                /*compression_threshold_bytes_in=*/0, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto proto_log = std::move(create_result.proto_log);
+    ASSERT_FALSE(create_result.has_data_loss());
+
+    // Sanity check: empty checksum.
+    ASSERT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(0u))));
+
+    // Writes 3 protos
+    ICING_ASSERT_OK(proto_log->WriteProto(document1));
+    ICING_ASSERT_OK(proto_log->WriteProto(document2));
+    ICING_ASSERT_OK(proto_log->WriteProto(document3));
+    // Although we didn't call UpdateChecksum() or PersistToDisk(),
+    // GetChecksum() should return the value reflecting the new written proto.
+    EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(3230460271u))));
+  }  // New checksum is written to the header and flushed in destructor.
+
+  // Erase data after the rewind position.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+      PortableFileBackedProtoLog<DocumentProto>::Create(
+          &filesystem_, file_path_,
+          PortableFileBackedProtoLog<DocumentProto>::Options(
+              compress_, max_proto_size_, compression_level_,
+              /*compression_threshold_bytes_in=*/0, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
+  auto proto_log = std::move(create_result.proto_log);
+  ASSERT_FALSE(create_result.has_data_loss());
+
+  // Sanity check that the checksum is identical to the previous one.
+  ASSERT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(3230460271u))));
+
+  // Write 2 more documents to the unsynced tail.
+  ICING_ASSERT_OK_AND_ASSIGN(int64_t document4_offset,
+                             proto_log->WriteProto(document4));
+  ICING_ASSERT_OK(proto_log->WriteProto(document5));
+  // Although we didn't call UpdateChecksum() or PersistToDisk(),
+  // GetChecksum() should return the value reflecting the new written proto.
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(705827520u))));
+
+  // Read the header.
+  Header header_before_erase = ReadHeader(filesystem_, file_path_);
+
+  // Erases the 4th proto that is after the rewind position. GetChecksum()
+  // should return the new checksum reflecting the erased proto.
+  ICING_ASSERT_OK(proto_log->EraseProto(document4_offset));
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(1052805596u))));
+
+  // Manually check the header.
+  Header header_after_erase = ReadHeader(filesystem_, file_path_);
+  EXPECT_THAT(header_after_erase.GetLegacyHeaderChecksum(),
+              Eq(header_before_erase.GetLegacyHeaderChecksum()));
+  EXPECT_THAT(header_after_erase.GetRewindOffset(),
+              Eq(header_before_erase.GetRewindOffset()));
+  EXPECT_THAT(header_after_erase.GetLogChecksum(),
+              Eq(header_before_erase.GetLogChecksum()));
+  if (GetParam()) {
+    // If the flag is enabled, when erasing document4, the unsynced tail
+    // checksum and header checksum should be updated.
+    EXPECT_THAT(header_after_erase.GetUnsyncedTailChecksum(),
+                Ne(header_before_erase.GetUnsyncedTailChecksum()));
+    EXPECT_THAT(header_after_erase.GetHeaderChecksum(),
+                Ne(header_before_erase.GetHeaderChecksum()));
+  } else {
+    // If the flag is disabled, the unsynced tail checksum and header checksum
+    // should remain 0.
+    EXPECT_THAT(header_after_erase.GetUnsyncedTailChecksum(), Eq(0u));
+    EXPECT_THAT(header_after_erase.GetHeaderChecksum(), Eq(0u));
+  }
+}
+
+TEST_P(PortableFileBackedProtoLogTest,
+       ErasedProtoBeforeRewindOffsetShouldUpdateHeader) {
+  DocumentProto document1 =
+      DocumentBuilder().SetKey("namespace", "uri1").Build();
+  DocumentProto document2 =
+      DocumentBuilder().SetKey("namespace", "uri2").Build();
+  DocumentProto document3 =
+      DocumentBuilder().SetKey("namespace", "uri3").Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+      PortableFileBackedProtoLog<DocumentProto>::Create(
+          &filesystem_, file_path_,
+          PortableFileBackedProtoLog<DocumentProto>::Options(
+              compress_, max_proto_size_, compression_level_,
+              /*compression_threshold_bytes_in=*/0, compression_mem_level_,
+              enable_smaller_decompression_buffer_size_,
+              /*enable_new_header_format_in=*/GetParam(),
+              /*enable_reusable_decompression_buffer=*/true)));
+  auto proto_log = std::move(create_result.proto_log);
+  ASSERT_FALSE(create_result.has_data_loss());
+
+  // Sanity check: empty checksum.
+  ASSERT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(0u))));
+
+  // Writes 3 protos
+  ICING_ASSERT_OK(proto_log->WriteProto(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(int64_t document2_offset,
+                             proto_log->WriteProto(document2));
+  ICING_ASSERT_OK(proto_log->WriteProto(document3));
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(3230460271u))));
+
+  // Persist to disk in order to make document1 to document3 in the log
+  // checksummed section.
+  ICING_ASSERT_OK(proto_log->PersistToDisk());
+
+  // Read the header.
+  Header header_before_erase = ReadHeader(filesystem_, file_path_);
+
+  // Erases the 2nd proto that is before the rewind position. GetChecksum()
+  // should return the new checksum reflecting the erased proto.
+  ICING_ASSERT_OK(proto_log->EraseProto(document2_offset));
+  EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(1845730629u))));
+
+  // Manually check the header:
+  // - Log checksum is updated because the log checksummed section was updated.
+  // - Legacy header checksum is updated because log checksum was updated.
+  // - RewindOffset should remain the same.
+  // - (Only if enable_new_header_format flag is true) Header checksum is
+  //   updated because the header was updated.
+  Header header_after_erase = ReadHeader(filesystem_, file_path_);
+  EXPECT_THAT(header_after_erase.GetLegacyHeaderChecksum(),
+              Ne(header_before_erase.GetLegacyHeaderChecksum()));
+  EXPECT_THAT(header_after_erase.GetRewindOffset(),
+              Eq(header_before_erase.GetRewindOffset()));
+  EXPECT_THAT(header_after_erase.GetLogChecksum(),
+              Ne(header_before_erase.GetLogChecksum()));
+  if (GetParam()) {
+    EXPECT_THAT(header_after_erase.GetHeaderChecksum(),
+                Ne(header_before_erase.GetHeaderChecksum()));
+  }
+}
+
+TEST_P(PortableFileBackedProtoLogTest,
+       ReadWriteHeaderShouldIncludePaddingBytes) {
+  // The header should have some padding bytes to make it 8-byte aligned.
+  static_assert(Header::kLegacyHeaderSectionSize -
+                    Header::kLegacyHeaderSectionPaddingOffset >
+                0);
+  std::string bytes = "abc";
+
+  DocumentProto document1 =
+      DocumentBuilder().SetKey("namespace", "uri1").Build();
+  DocumentProto document2 =
+      DocumentBuilder().SetKey("namespace", "uri2").Build();
+  DocumentProto document3 =
+      DocumentBuilder().SetKey("namespace", "uri3").Build();
+
+  {
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                compress_, max_proto_size_, compression_level_,
+                /*compression_threshold_bytes_in=*/0, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto proto_log = std::move(create_result.proto_log);
+    ASSERT_FALSE(create_result.has_data_loss());
+
+    // Sanity check: empty checksum.
+    ASSERT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(0u))));
+
+    // Writes 2 protos
+    ICING_ASSERT_OK(proto_log->WriteProto(document1));
+    ICING_ASSERT_OK(proto_log->WriteProto(document2));
+    EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(1632103647u))));
+    // Persist to disk.
+    ICING_ASSERT_OK(proto_log->PersistToDisk());
+
+    // Overwrite the padding bytes with some random bytes.
+    Header* header = proto_log->header();
+    memcpy(reinterpret_cast<char*>(header) +
+               Header::kLegacyHeaderSectionPaddingOffset,
+           bytes.data(),
+           Header::kLegacyHeaderSectionSize -
+               Header::kLegacyHeaderSectionPaddingOffset);
+
+    // Add 1 more proto. It is a hack to make the proto log dirty and make the
+    // next PersistToDisk take effect.
+    ICING_ASSERT_OK(proto_log->WriteProto(document3));
+    EXPECT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(3230460271u))));
+    ICING_ASSERT_OK(proto_log->PersistToDisk());
+
+    // Manually check the header: the padding bytes contain the overwritten
+    // values.
+    // Ensures that write header operation also includes the padding bytes.
+    Header header_after = ReadHeader(filesystem_, file_path_);
+    EXPECT_THAT(std::string(reinterpret_cast<const char*>(&header_after) +
+                                Header::kLegacyHeaderSectionPaddingOffset,
+                            Header::kLegacyHeaderSectionSize -
+                                Header::kLegacyHeaderSectionPaddingOffset),
+                Eq(bytes));
+  }
+
+  {
+    ICING_ASSERT_OK_AND_ASSIGN(
+        PortableFileBackedProtoLog<DocumentProto>::CreateResult create_result,
+        PortableFileBackedProtoLog<DocumentProto>::Create(
+            &filesystem_, file_path_,
+            PortableFileBackedProtoLog<DocumentProto>::Options(
+                compress_, max_proto_size_, compression_level_,
+                /*compression_threshold_bytes_in=*/0, compression_mem_level_,
+                enable_smaller_decompression_buffer_size_,
+                /*enable_new_header_format_in=*/GetParam(),
+                /*enable_reusable_decompression_buffer=*/true)));
+    auto proto_log = std::move(create_result.proto_log);
+    ASSERT_FALSE(create_result.has_data_loss());
+
+    // Sanity check that the checksum is identical to the previous one.
+    ASSERT_THAT(proto_log->GetChecksum(), IsOkAndHolds(Eq(Crc32(3230460271u))));
+
+    // Verify that read header operation includes the padding bytes.
+    Header* header = proto_log->header();
+    EXPECT_THAT(std::string(reinterpret_cast<const char*>(header) +
+                                Header::kLegacyHeaderSectionPaddingOffset,
+                            Header::kLegacyHeaderSectionSize -
+                                Header::kLegacyHeaderSectionPaddingOffset),
+                Eq(bytes));
+  }
+}
+
+TEST_P(PortableFileBackedProtoLogTest,
+       CalculateLegacyHeaderChecksumShouldIncludePadding) {
+  // The legacy header should have some padding bytes to make it 8-byte aligned.
+  static_assert(Header::kLegacyHeaderSectionSize -
+                    Header::kLegacyHeaderSectionPaddingOffset >
+                0);
+  std::string bytes = "abc";
+
+  // Create a header instance and set all bytes to 1 for the header.
+  // Note that -1 means 0b111111...111.
+  Header header;
+  memset(&header, -1, sizeof(Header));
+  EXPECT_THAT(header.CalculateLegacyHeaderChecksum(), Eq(2132597986u));
+
+  // Set different bytes to the padding section. Verify that the legacy header
+  // checksum reflects the padding byte changes.
+  memcpy(reinterpret_cast<char*>(&header) +
+             Header::kLegacyHeaderSectionPaddingOffset,
+         bytes.data(),
+         Header::kLegacyHeaderSectionSize -
+             Header::kLegacyHeaderSectionPaddingOffset);
+  EXPECT_THAT(header.CalculateLegacyHeaderChecksum(), Eq(3049742880u));
+}
+
+INSTANTIATE_TEST_SUITE_P(PortableFileBackedProtoLogTest,
+                         PortableFileBackedProtoLogTest,
+                         testing::Values(true, false));
+
 }  // namespace
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/file/portable-file-backed-proto-log_thread-safety_test.cc b/icing/file/portable-file-backed-proto-log_thread-safety_test.cc
new file mode 100644
index 0000000..1797e98
--- /dev/null
+++ b/icing/file/portable-file-backed-proto-log_thread-safety_test.cc
@@ -0,0 +1,117 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <memory>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "icing/document-builder.h"
+#include "icing/file/filesystem.h"
+#include "icing/file/portable-file-backed-proto-log.h"
+#include "icing/portable/equals-proto.h"
+#include "icing/proto/document.pb.h"
+#include "icing/testing/common-matchers.h"
+#include "icing/testing/tmp-directory.h"
+
+namespace icing {
+namespace lib {
+namespace {
+
+using ::icing::lib::portable_equals_proto::EqualsProto;
+using ::testing::NotNull;
+
+class PortableFileBackedProtoLogThreadSafetyTest
+    : public ::testing::TestWithParam<bool> {
+ protected:
+  void SetUp() override {
+    file_path_ = GetTestTempDir() + "/proto_log_thread_safety";
+    filesystem_.DeleteFile(file_path_.c_str());
+  }
+
+  void TearDown() override { filesystem_.DeleteFile(file_path_.c_str()); }
+
+  const Filesystem filesystem_;
+  std::string file_path_;
+};
+
+TEST_P(PortableFileBackedProtoLogThreadSafetyTest, ConcurrentReadProto) {
+  // Write some documents to the log.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      auto create_result,
+      PortableFileBackedProtoLog<DocumentProto>::Create(
+          &filesystem_, file_path_,
+          PortableFileBackedProtoLog<DocumentProto>::Options(
+              /*compress_in=*/true,
+              /*max_proto_size_in=*/1024 * 1024,
+              PortableFileBackedProtoLog<
+                  DocumentProto>::kDefaultCompressionLevel,
+              PortableFileBackedProtoLog<
+                  DocumentProto>::kDefaultCompressionThresholdBytes,
+              /*compression_mem_level_in=*/1,
+              /*enable_smaller_decompression_buffer_size_in=*/true,
+              /*enable_new_header_format_in=*/true,
+              /*enable_reusable_decompression_buffer_in=*/GetParam())));
+  std::unique_ptr<PortableFileBackedProtoLog<DocumentProto>> proto_log =
+      std::move(create_result.proto_log);
+  ASSERT_THAT(proto_log, NotNull());
+
+  constexpr int kNumDocuments = 20;
+  std::vector<DocumentProto> documents;
+  std::vector<int64_t> offsets;
+  for (int i = 0; i < kNumDocuments; ++i) {
+    documents.push_back(
+        DocumentBuilder()
+            .SetKey("namespace", "uri" + std::to_string(i))
+            .AddStringProperty("prop", std::string(100 * i, 'a'))
+            .Build());
+    ICING_ASSERT_OK_AND_ASSIGN(int64_t offset,
+                               proto_log->WriteProto(documents.back()));
+    offsets.push_back(offset);
+  }
+  ICING_ASSERT_OK(proto_log->PersistToDisk());
+
+  // Create kNumThreads to call ReadProto concurrently.
+  constexpr int kNumThreads = 50;
+  constexpr int kNumReadsPerThread = 100;
+
+  auto reader_task = [&]() {
+    for (int i = 0; i < kNumReadsPerThread; ++i) {
+      int doc_index = i % kNumDocuments;
+      ICING_ASSERT_OK_AND_ASSIGN(DocumentProto read_doc,
+                                 proto_log->ReadProto(offsets[doc_index]));
+      EXPECT_THAT(read_doc, EqualsProto(documents[doc_index]));
+    }
+  };
+
+  std::vector<std::thread> threads;
+  threads.reserve(kNumThreads);
+  for (int i = 0; i < kNumThreads; ++i) {
+    threads.emplace_back(reader_task);
+  }
+
+  for (auto& thread : threads) {
+    thread.join();
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(PortableFileBackedProtoLogThreadSafetyTest,
+                         PortableFileBackedProtoLogThreadSafetyTest,
+                         testing::Values(true, false));
+
+}  // namespace
+}  // namespace lib
+}  // namespace icing
\ No newline at end of file
diff --git a/icing/file/posting_list/flash-index-storage-header.h b/icing/file/posting_list/flash-index-storage-header.h
index f7b331c..67fc4a6 100644
--- a/icing/file/posting_list/flash-index-storage-header.h
+++ b/icing/file/posting_list/flash-index-storage-header.h
@@ -62,7 +62,7 @@
   static libtextclassifier3::StatusOr<HeaderBlock> Read(
       const Filesystem* filesystem, int fd, int block_size) {
     std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(block_size);
-    if (!filesystem->PRead(fd, buffer.get(), block_size, 0)) {
+    if (filesystem->PRead(fd, buffer.get(), block_size, 0) != block_size) {
       return absl_ports::InternalError("Unable to reader header block!");
     }
     return HeaderBlock(filesystem, std::move(buffer), block_size);
diff --git a/icing/file/posting_list/flash-index-storage.cc b/icing/file/posting_list/flash-index-storage.cc
index 2198d2c..68f8e4c 100644
--- a/icing/file/posting_list/flash-index-storage.cc
+++ b/icing/file/posting_list/flash-index-storage.cc
@@ -21,15 +21,23 @@
 #include <cerrno>
 #include <cinttypes>
 #include <cstdint>
+#include <cstring>
 #include <memory>
+#include <string>
+#include <utility>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/absl_ports/str_cat.h"
+#include "icing/file/filesystem.h"
+#include "icing/file/posting_list/flash-index-storage-header.h"
 #include "icing/file/posting_list/index-block.h"
 #include "icing/file/posting_list/posting-list-common.h"
+#include "icing/file/posting_list/posting-list-identifier.h"
+#include "icing/file/posting_list/posting-list-used.h"
 #include "icing/legacy/core/icing-string-util.h"
+#include "icing/store/document-id.h"
 #include "icing/util/logging.h"
 #include "icing/util/math-util.h"
 #include "icing/util/status-macros.h"
@@ -110,20 +118,24 @@
   // Look for an existing file size.
   int64_t file_size = filesystem_->GetFileSize(storage_sfd_.get());
   if (file_size == Filesystem::kBadFileSize) {
-    ICING_LOG(ERROR) << "Could not initialize main index. Bad file size.";
+    ICING_LOG(ERROR)
+        << "Could not initialize flash index storage. Bad file size. Path: "
+        << index_filename_;
     return false;
   }
 
   if (file_size == 0) {
     if (!CreateHeader()) {
-      ICING_LOG(ERROR)
-          << "Could not initialize main index. Unable to create header.";
+      ICING_LOG(ERROR) << "Could not initialize flash index storage. Unable to "
+                          "create header. Path: "
+                       << index_filename_;
       return false;
     }
   } else {
     if (!OpenHeader(file_size)) {
-      ICING_LOG(ERROR)
-          << "Could not initialize main index. Unable to open header.";
+      ICING_LOG(ERROR) << "Could not initialize flash index storage. Unable to "
+                          "open header. Path: "
+                       << index_filename_;
       return false;
     }
   }
@@ -181,25 +193,32 @@
       HeaderBlock read_header,
       HeaderBlock::Read(filesystem_, storage_sfd_.get(), block_size), false);
   if (read_header.header()->magic != HeaderBlock::Header::kMagic) {
-    ICING_LOG(ERROR) << "Index header block wrong magic";
+    ICING_LOG(ERROR) << "Invalid FlashIndexStorage header for "
+                     << index_filename_ << ": wrong magic. Expected: "
+                     << HeaderBlock::Header::kMagic
+                     << ", actual: " << read_header.header()->magic;
     return false;
   }
   if (file_size % read_header.header()->block_size != 0) {
-    ICING_LOG(ERROR) << "Index size " << file_size
-                     << " not a multiple of block size "
+    ICING_LOG(ERROR) << "Invalid FlashIndexStorage header for "
+                     << index_filename_ << ": file size " << file_size
+                     << " is not a multiple of block size "
                      << read_header.header()->block_size;
     return false;
   }
 
   if (file_size < static_cast<int64_t>(read_header.header()->block_size)) {
-    ICING_LOG(ERROR) << "Index size " << file_size
-                     << " shorter than block size "
+    ICING_LOG(ERROR) << "Invalid FlashIndexStorage header for "
+                     << index_filename_ << ": file size " << file_size
+                     << " is shorter than block size "
                      << read_header.header()->block_size;
     return false;
   }
 
   if (read_header.header()->block_size % getpagesize() != 0) {
-    ICING_LOG(ERROR) << "Block size " << read_header.header()->block_size
+    ICING_LOG(ERROR) << "Invalid FlashIndexStorage header for "
+                     << index_filename_ << ": block size "
+                     << read_header.header()->block_size
                      << " is not a multiple of page size " << getpagesize();
     return false;
   }
@@ -211,7 +230,8 @@
     // still use the main index, but reads/writes won't be as efficient in terms
     // of flash IO because the 'blocks' that we're reading are actually multiple
     // pages long.
-    ICING_LOG(ERROR) << "Block size of existing header ("
+    ICING_LOG(ERROR) << "Invalid FlashIndexStorage header for "
+                     << index_filename_ << ": block size of existing header ("
                      << read_header.header()->block_size
                      << ") does not match the requested block size ("
                      << block_size << "). Defaulting to existing block size "
@@ -244,7 +264,8 @@
 bool FlashIndexStorage::PersistToDisk() {
   // First, write header.
   if (!header_block_->Write(storage_sfd_.get())) {
-    ICING_LOG(ERROR) << "Write index header failed: " << strerror(errno);
+    ICING_LOG(ERROR) << "Write index header failed: (" << errno << ") "
+                     << strerror(errno);
     return false;
   }
 
@@ -535,7 +556,8 @@
   if (!filesystem_->Grow(
           storage_sfd_.get(),
           static_cast<uint64_t>(num_blocks_ + 1) * block_size())) {
-    ICING_VLOG(1) << "Error growing index file: " << strerror(errno);
+    ICING_LOG(ERROR) << "Error growing index file: (" << errno << ") "
+                     << strerror(errno);
     return kInvalidBlockIndex;
   }
 
diff --git a/icing/file/posting_list/index-block.cc b/icing/file/posting_list/index-block.cc
index 3fa397c..1f47da2 100644
--- a/icing/file/posting_list/index-block.cc
+++ b/icing/file/posting_list/index-block.cc
@@ -67,7 +67,8 @@
   }
 
   BlockHeader header;
-  if (!filesystem->PRead(fd, &header, sizeof(BlockHeader), block_file_offset)) {
+  if (filesystem->PRead(fd, &header, sizeof(BlockHeader), block_file_offset) !=
+      sizeof(BlockHeader)) {
     return absl_ports::InternalError("PRead block header error");
   }
 
@@ -284,8 +285,8 @@
 libtextclassifier3::StatusOr<IndexBlock::BlockHeader> IndexBlock::ReadHeader()
     const {
   BlockHeader header;
-  if (!filesystem_->PRead(fd_, &header, sizeof(BlockHeader),
-                          block_file_offset_)) {
+  if (filesystem_->PRead(fd_, &header, sizeof(BlockHeader),
+                         block_file_offset_) != sizeof(BlockHeader)) {
     return absl_ports::InternalError(
         absl_ports::StrCat("PRead block header error: ", strerror(errno)));
   }
@@ -301,8 +302,9 @@
 libtextclassifier3::StatusOr<std::unique_ptr<uint8_t[]>>
 IndexBlock::ReadPostingList(PostingListIndex posting_list_index) const {
   auto posting_list_buffer = std::make_unique<uint8_t[]>(posting_list_bytes_);
-  if (!filesystem_->PRead(fd_, posting_list_buffer.get(), posting_list_bytes_,
-                          get_posting_list_file_offset(posting_list_index))) {
+  if (filesystem_->PRead(fd_, posting_list_buffer.get(), posting_list_bytes_,
+                         get_posting_list_file_offset(posting_list_index)) !=
+      posting_list_bytes_) {
     return absl_ports::InternalError(
         absl_ports::StrCat("PRead posting list error: ", strerror(errno)));
   }
diff --git a/icing/file/version-util.cc b/icing/file/version-util.cc
index c44ac69..2bc2122 100644
--- a/icing/file/version-util.cc
+++ b/icing/file/version-util.cc
@@ -30,6 +30,7 @@
 #include "icing/file/filesystem.h"
 #include "icing/index/index.h"
 #include "icing/proto/initialize.pb.h"
+#include "icing/util/logging.h"
 #include "icing/util/status-macros.h"
 
 namespace icing {
@@ -45,10 +46,15 @@
   // 1. Read the version info.
   const std::string v1_version_filepath =
       MakeVersionFilePath(version_file_dir, kVersionFilenameV1);
+  bool file_exists = filesystem.FileExists(v1_version_filepath.c_str());
+  ICING_LOG(INFO) << "v1 version file exists: " << file_exists;
+
   VersionInfo existing_version_info(-1, -1);
-  if (filesystem.FileExists(v1_version_filepath.c_str()) &&
-      !filesystem.PRead(v1_version_filepath.c_str(), &existing_version_info,
-                        sizeof(VersionInfo), /*offset=*/0)) {
+  if (file_exists &&
+      filesystem.PRead(v1_version_filepath.c_str(), &existing_version_info,
+                       sizeof(VersionInfo),
+                       /*offset=*/0) != sizeof(VersionInfo)) {
+    ICING_LOG(ERROR) << "Failed to read v1 version file";
     return absl_ports::InternalError("Failed to read v1 version file");
   }
 
@@ -56,6 +62,12 @@
   libtextclassifier3::StatusOr<int> existing_flash_index_magic =
       Index::ReadFlashIndexMagic(&filesystem, index_base_dir);
   if (!existing_flash_index_magic.ok()) {
+    ICING_LOG(WARNING) << "Cannot read index flash index magic for v1 version "
+                          "check. Error code: "
+                       << existing_flash_index_magic.status().error_code()
+                       << ", message: "
+                       << existing_flash_index_magic.status().error_message();
+
     if (absl_ports::IsNotFound(existing_flash_index_magic.status())) {
       // Flash index magic doesn't exist. In this case, we're unable to
       // determine the version change state correctly (regardless of the
@@ -67,7 +79,10 @@
     // Real error.
     return std::move(existing_flash_index_magic).status();
   }
-  if (existing_flash_index_magic.ValueOrDie() == kVersionZeroFlashIndexMagic) {
+
+  int flash_index_magic = existing_flash_index_magic.ValueOrDie();
+  ICING_LOG(INFO) << "Index flash index magic: " << flash_index_magic;
+  if (flash_index_magic == kVersionZeroFlashIndexMagic) {
     existing_version_info.version = 0;
     if (existing_version_info.max_version == -1) {
       existing_version_info.max_version = 0;
@@ -83,6 +98,9 @@
   // IcingSearchEngineVersionProto as a file-backed proto.
   const std::string v2_version_filepath =
       MakeVersionFilePath(version_file_dir, kVersionFilenameV2);
+  ICING_LOG(INFO) << "v2 version file exists: "
+                  << filesystem.FileExists(v2_version_filepath.c_str());
+
   FileBackedProto<IcingSearchEngineVersionProto> v2_version_file(
       filesystem, v2_version_filepath);
   ICING_ASSIGN_OR_RETURN(const IcingSearchEngineVersionProto* v2_version_proto,
@@ -112,6 +130,11 @@
   // 2. Read the v2 version file
   auto v2_version_proto = ReadV2VersionInfo(filesystem, version_file_dir);
   if (!v2_version_proto.ok()) {
+    ICING_LOG(WARNING) << "Cannot read v2 version file. Error code: "
+                       << v2_version_proto.status().error_code()
+                       << ", message: "
+                       << v2_version_proto.status().error_message();
+
     if (!absl_ports::IsNotFound(v2_version_proto.status())) {
       // Real error.
       return std::move(v2_version_proto).status();
@@ -141,6 +164,11 @@
       // should have been written.
       // Return an invalid version number in this case and trigger rebuilding
       // everything.
+      ICING_LOG(WARNING)
+          << "v1 version file has a version number greater or equal to "
+             "kFirstV2Version, but failed to read v2 version file. Return "
+             "version -1 and rebuild everything.";
+
       version_proto.set_version(-1);
       version_proto.set_max_version(v1_version_info.max_version);
     }
@@ -153,6 +181,10 @@
   IcingSearchEngineVersionProto v2_version_proto_value =
       std::move(v2_version_proto).ValueOrDie();
   if (v1_version_info.version != v2_version_proto_value.version()) {
+    ICING_LOG(WARNING)
+        << "v1 and v2 version files have different version numbers, probably "
+           "due to roll forward. Return version -1 and rebuild everything.";
+
     v2_version_proto_value.set_version(-1);
     v2_version_proto_value.mutable_enabled_features()->Clear();
   }
@@ -165,26 +197,51 @@
                                           const VersionInfo& version_info) {
   ScopedFd scoped_fd(filesystem.OpenForWrite(
       MakeVersionFilePath(version_file_dir, kVersionFilenameV1).c_str()));
-  if (!scoped_fd.is_valid() ||
-      !filesystem.PWrite(scoped_fd.get(), /*offset=*/0, &version_info,
-                         sizeof(VersionInfo)) ||
-      !filesystem.DataSync(scoped_fd.get())) {
+  if (!scoped_fd.is_valid()) {
+    ICING_LOG(ERROR) << "Failed to open v1 version file for write";
+    return absl_ports::InternalError(
+        "Failed to open v1 version file for write");
+  }
+
+  if (!filesystem.PWrite(scoped_fd.get(), /*offset=*/0, &version_info,
+                         sizeof(VersionInfo))) {
+    ICING_LOG(ERROR) << "Failed to write v1 version file";
     return absl_ports::InternalError("Failed to write v1 version file");
   }
+
+  if (!filesystem.DataSync(scoped_fd.get())) {
+    ICING_LOG(ERROR) << "Failed to sync v1 version file";
+    return absl_ports::InternalError("Failed to sync v1 version file");
+  }
+
+  ICING_LOG(INFO)
+      << "Successfully write and sync v1 version file with version = "
+      << version_info.version;
   return libtextclassifier3::Status::OK;
 }
 
 libtextclassifier3::Status WriteV2Version(
     const Filesystem& filesystem, const std::string& version_file_dir,
     std::unique_ptr<IcingSearchEngineVersionProto> version_proto) {
+  int32_t version = version_proto->version();
+
+  // FileBackedProto::Write also syncs the file, so we don't need to call
+  // DataSync explicitly.
   FileBackedProto<IcingSearchEngineVersionProto> v2_version_file(
       filesystem, MakeVersionFilePath(version_file_dir, kVersionFilenameV2));
   libtextclassifier3::Status v2_write_status =
       v2_version_file.Write(std::move(version_proto));
   if (!v2_write_status.ok()) {
+    ICING_LOG(ERROR) << "Failed to write v2 version file. Error code: "
+                     << v2_write_status.error_code()
+                     << ", message: " << v2_write_status.error_message();
     return absl_ports::InternalError(absl_ports::StrCat(
         "Failed to write v2 version file: ", v2_write_status.error_message()));
   }
+
+  ICING_LOG(INFO)
+      << "Successfully write and sync v2 version file with version = "
+      << version;
   return libtextclassifier3::Status::OK;
 }
 
@@ -329,6 +386,18 @@
         // version 5 -> version 6 upgrade, no need to rebuild
         break;
       }
+      case 6: {
+        // version 6 -> version 7 upgrade, no need to rebuild
+        break;
+      }
+      case 7: {
+        // version 7 -> version 8 upgrade, no need to rebuild
+        break;
+      }
+      case 8: {
+        // version 8 -> version 9 upgrade, no need to rebuild
+        break;
+      }
       default:
         // This should not happen. Rebuild anyway if unsure.
         should_rebuild |= true;
@@ -389,8 +458,37 @@
           /*needs_qualified_id_join_index_rebuild=*/false,
           /*needs_embedding_index_rebuild=*/false);
     }
+    case IcingSearchEngineFeatureInfoProto::FEATURE_QUALIFIED_ID_JOIN_INDEX_V3:
     case IcingSearchEngineFeatureInfoProto::
-        FEATURE_QUALIFIED_ID_JOIN_INDEX_V3: {
+        FEATURE_NON_EXISTENT_QUALIFIED_ID_JOIN: {
+      return derived_file_util::DerivedFilesRebuildInfo(
+          /*needs_document_store_derived_files_rebuild=*/false,
+          /*needs_schema_store_derived_files_rebuild=*/false,
+          /*needs_term_index_rebuild=*/false,
+          /*needs_integer_index_rebuild=*/false,
+          /*needs_qualified_id_join_index_rebuild=*/true,
+          /*needs_embedding_index_rebuild=*/false);
+    }
+    case IcingSearchEngineFeatureInfoProto::FEATURE_DELETE_PROPAGATION_FROM: {
+      // By rebuilding qualified id join index, we will re-validate document
+      // dependency: if a schema enables delete propagation but a document of
+      // this schema references a deleted/expired/non-existent parent document,
+      // then Icing will remove this child document.
+      //
+      // Note: document dependency re-validation can be done by rebuilding any
+      //   index since the process is done in TokenizedDocument generation, but
+      //   we choose to rebuild the join index because:
+      //   - Rebuilding the join index guarantees to "replay" document protos
+      //     from doc id 0. All alive documents will be iterated and
+      //     re-validated.
+      //   - Delete propagation is related to the join index.
+      //   - We could've only generated TokenizedDocument and re-validated the
+      //     document dependency without rebuilding any indices, but:
+      //     - This would require more changes to Icing's derived files recovery
+      //       logic (i.e. RestoreIndexIfNeeded()).
+      //     - Tt is rare to switch this flag on and off too frequently.
+      //     - Therefore, let's just simply reuse the existing derived files
+      //       recovery logic to trigger document dependency re-validation.
       return derived_file_util::DerivedFilesRebuildInfo(
           /*needs_document_store_derived_files_rebuild=*/false,
           /*needs_schema_store_derived_files_rebuild=*/false,
@@ -478,6 +576,17 @@
     enabled_features->Add(GetFeatureInfoProto(
         IcingSearchEngineFeatureInfoProto::FEATURE_QUALIFIED_ID_JOIN_INDEX_V3));
   }
+  // DeletePropagation PROPAGATE_FROM feature
+  if (options.enable_delete_propagation_from()) {
+    enabled_features->Add(GetFeatureInfoProto(
+        IcingSearchEngineFeatureInfoProto::FEATURE_DELETE_PROPAGATION_FROM));
+  }
+  // NonExistentQualifiedIdJoin feature
+  if (options.enable_non_existent_qualified_id_join()) {
+    enabled_features->Add(
+        GetFeatureInfoProto(IcingSearchEngineFeatureInfoProto::
+                                FEATURE_NON_EXISTENT_QUALIFIED_ID_JOIN));
+  }
 }
 
 }  // namespace version_util
diff --git a/icing/file/version-util.h b/icing/file/version-util.h
index 4b32e0c..3ee187c 100644
--- a/icing/file/version-util.h
+++ b/icing/file/version-util.h
@@ -39,7 +39,7 @@
 // - Version 3: M-2024-02. Schema is compatible with v1 and v2.
 // - Version 4: Android V base. Schema is compatible with v1, v2 and v3.
 // - Version 5: M-2025-02. Schema is compatible with v1, v2, v3 and v4.
-inline static constexpr int32_t kVersion = 6;
+inline static constexpr int32_t kVersion = 9;
 inline static constexpr int32_t kVersionOne = 1;
 inline static constexpr int32_t kVersionTwo = 2;
 inline static constexpr int32_t kVersionThree = 3;
diff --git a/icing/file/version-util_test.cc b/icing/file/version-util_test.cc
index deec9d1..ca54af9 100644
--- a/icing/file/version-util_test.cc
+++ b/icing/file/version-util_test.cc
@@ -1076,6 +1076,92 @@
                 /*needs_term_index_rebuild_in=*/false,
                 /*needs_integer_index_rebuild_in=*/false,
                 /*needs_qualified_id_join_index_rebuild_in=*/true,
+                /*needs_embedding_index_rebuild_in=*/false)),
+
+        // - Existing version 8, max_version 8
+        // - Existing enabled features = {}
+        // - Current version = 8
+        // - Current enabled features = {FEATURE_DELETE_PROPAGATION_FROM}
+        //
+        // - Result: rebuild qualified id join index
+        VersionUtilDerivedFilesRebuildTestParam(
+            /*existing_version_in=*/8, /*max_version_in=*/8,
+            /*existing_enabled_features_in=*/{},
+            /*curr_version_in=*/8, /*curr_enabled_features_in=*/
+            {IcingSearchEngineFeatureInfoProto::
+                 FEATURE_DELETE_PROPAGATION_FROM},
+            /*expected_derived_files_rebuild_info_in=*/
+            DerivedFilesRebuildInfo(
+                /*needs_document_store_derived_files_rebuild_in=*/false,
+                /*needs_schema_store_derived_files_rebuild_in=*/false,
+                /*needs_term_index_rebuild_in=*/false,
+                /*needs_integer_index_rebuild_in=*/false,
+                /*needs_qualified_id_join_index_rebuild_in=*/true,
+                /*needs_embedding_index_rebuild_in=*/false)),
+
+        // - Existing version 8, max_version 8
+        // - Existing enabled features =
+        //   {FEATURE_DELETE_PROPAGATION_FROM}
+        // - Current version = 8
+        // - Current enabled features = {}
+        //
+        // - Result: rebuild qualified id join index
+        VersionUtilDerivedFilesRebuildTestParam(
+            /*existing_version_in=*/8, /*max_version_in=*/8,
+            /*existing_enabled_features_in=*/
+            {IcingSearchEngineFeatureInfoProto::
+                 FEATURE_DELETE_PROPAGATION_FROM},
+            /*curr_version_in=*/8, /*curr_enabled_features_in=*/{},
+            /*expected_derived_files_rebuild_info_in=*/
+            DerivedFilesRebuildInfo(
+                /*needs_document_store_derived_files_rebuild_in=*/false,
+                /*needs_schema_store_derived_files_rebuild_in=*/false,
+                /*needs_term_index_rebuild_in=*/false,
+                /*needs_integer_index_rebuild_in=*/false,
+                /*needs_qualified_id_join_index_rebuild_in=*/true,
+                /*needs_embedding_index_rebuild_in=*/false)),
+
+        // - Existing version 5, max_version 5
+        // - Existing enabled features = {}
+        // - Current version = 5
+        // - Current enabled features = {FEATURE_NON_EXISTENT_QUALIFIED_ID_JOIN}
+        //
+        // - Result: rebuild qualified id join index
+        VersionUtilDerivedFilesRebuildTestParam(
+            /*existing_version_in=*/5, /*max_version_in=*/5,
+            /*existing_enabled_features_in=*/{},
+            /*curr_version_in=*/5, /*curr_enabled_features_in=*/
+            {IcingSearchEngineFeatureInfoProto::
+                 FEATURE_NON_EXISTENT_QUALIFIED_ID_JOIN},
+            /*expected_derived_files_rebuild_info_in=*/
+            DerivedFilesRebuildInfo(
+                /*needs_document_store_derived_files_rebuild_in=*/false,
+                /*needs_schema_store_derived_files_rebuild_in=*/false,
+                /*needs_term_index_rebuild_in=*/false,
+                /*needs_integer_index_rebuild_in=*/false,
+                /*needs_qualified_id_join_index_rebuild_in=*/true,
+                /*needs_embedding_index_rebuild_in=*/false)),
+
+        // - Existing version 5, max_version 5
+        // - Existing enabled features =
+        //   {FEATURE_NON_EXISTENT_QUALIFIED_ID_JOIN}
+        // - Current version = 5
+        // - Current enabled features = {}
+        //
+        // - Result: rebuild qualified id join index
+        VersionUtilDerivedFilesRebuildTestParam(
+            /*existing_version_in=*/5, /*max_version_in=*/5,
+            /*existing_enabled_features_in=*/
+            {IcingSearchEngineFeatureInfoProto::
+                 FEATURE_NON_EXISTENT_QUALIFIED_ID_JOIN},
+            /*curr_version_in=*/5, /*curr_enabled_features_in=*/{},
+            /*expected_derived_files_rebuild_info_in=*/
+            DerivedFilesRebuildInfo(
+                /*needs_document_store_derived_files_rebuild_in=*/false,
+                /*needs_schema_store_derived_files_rebuild_in=*/false,
+                /*needs_term_index_rebuild_in=*/false,
+                /*needs_integer_index_rebuild_in=*/false,
+                /*needs_qualified_id_join_index_rebuild_in=*/true,
                 /*needs_embedding_index_rebuild_in=*/false))));
 
 TEST(VersionUtilTest, ShouldRebuildDerivedFilesUndeterminedVersion) {
@@ -1293,6 +1379,34 @@
           /*needs_embedding_index_rebuild_in=*/false)));
 }
 
+TEST(VersionUtilTest,
+     GetFeatureDerivedFilesRebuildInfo_featureDeletePropagationFrom) {
+  EXPECT_THAT(
+      GetFeatureDerivedFilesRebuildInfo(
+          IcingSearchEngineFeatureInfoProto::FEATURE_DELETE_PROPAGATION_FROM),
+      Eq(DerivedFilesRebuildInfo(
+          /*needs_document_store_derived_files_rebuild_in=*/false,
+          /*needs_schema_store_derived_files_rebuild_in=*/false,
+          /*needs_term_index_rebuild_in=*/false,
+          /*needs_integer_index_rebuild_in=*/false,
+          /*needs_qualified_id_join_index_rebuild_in=*/true,
+          /*needs_embedding_index_rebuild_in=*/false)));
+}
+
+TEST(VersionUtilTest,
+     GetFeatureDerivedFilesRebuildInfo_featureNonExistentQualifiedIdJoin) {
+  EXPECT_THAT(GetFeatureDerivedFilesRebuildInfo(
+                  IcingSearchEngineFeatureInfoProto::
+                      FEATURE_NON_EXISTENT_QUALIFIED_ID_JOIN),
+              Eq(DerivedFilesRebuildInfo(
+                  /*needs_document_store_derived_files_rebuild_in=*/false,
+                  /*needs_schema_store_derived_files_rebuild_in=*/false,
+                  /*needs_term_index_rebuild_in=*/false,
+                  /*needs_integer_index_rebuild_in=*/false,
+                  /*needs_qualified_id_join_index_rebuild_in=*/true,
+                  /*needs_embedding_index_rebuild_in=*/false)));
+}
+
 TEST(VersionUtilTest, SchemaDatabaseMigrationRequired) {
   // Migration is required if the previous version is less than the version at
   // which the database field is introduced.
@@ -1365,6 +1479,34 @@
                                 FEATURE_QUALIFIED_ID_JOIN_INDEX_V3))));
 }
 
+TEST(VersionUtilTest,
+     IcingSearchEngineOptionsToVersionProto_deletePropagationFrom_enabled) {
+  IcingSearchEngineOptions options;
+  options.set_enable_delete_propagation_from(true);
+
+  IcingSearchEngineVersionProto version_proto;
+  AddEnabledFeatures(options, &version_proto);
+  EXPECT_THAT(
+      version_proto.enabled_features(),
+      Contains(Property(
+          &IcingSearchEngineFeatureInfoProto::feature_type,
+          IcingSearchEngineFeatureInfoProto::FEATURE_DELETE_PROPAGATION_FROM)));
+}
+
+TEST(VersionUtilTest,
+     IcingSearchEngineOptionsToVersionProto_deletePropagationFrom_disabled) {
+  IcingSearchEngineOptions options;
+  options.set_enable_delete_propagation_from(false);
+
+  IcingSearchEngineVersionProto version_proto;
+  AddEnabledFeatures(options, &version_proto);
+  EXPECT_THAT(
+      version_proto.enabled_features(),
+      Not(Contains(Property(&IcingSearchEngineFeatureInfoProto::feature_type,
+                            IcingSearchEngineFeatureInfoProto::
+                                FEATURE_DELETE_PROPAGATION_FROM))));
+}
+
 class VersionUtilFeatureProtoTest
     : public ::testing::TestWithParam<
           IcingSearchEngineFeatureInfoProto::FlaggedFeatureType> {};
@@ -1402,7 +1544,10 @@
         IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_INDEX,
         IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_QUANTIZATION,
         IcingSearchEngineFeatureInfoProto::FEATURE_SCHEMA_DATABASE,
-        IcingSearchEngineFeatureInfoProto::FEATURE_QUALIFIED_ID_JOIN_INDEX_V3));
+        IcingSearchEngineFeatureInfoProto::FEATURE_QUALIFIED_ID_JOIN_INDEX_V3,
+        IcingSearchEngineFeatureInfoProto::FEATURE_DELETE_PROPAGATION_FROM,
+        IcingSearchEngineFeatureInfoProto::
+            FEATURE_NON_EXISTENT_QUALIFIED_ID_JOIN));
 
 }  // namespace
 
diff --git a/icing/graph/graph-interface.h b/icing/graph/graph-interface.h
new file mode 100644
index 0000000..6605f28
--- /dev/null
+++ b/icing/graph/graph-interface.h
@@ -0,0 +1,74 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef ICING_GRAPH_GRAPH_INTERFACE_H_
+#define ICING_GRAPH_GRAPH_INTERFACE_H_
+
+#include <memory>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+
+namespace icing {
+namespace lib {
+
+namespace graph {
+
+// Graph interface with integer node id and given EdgeType.
+template <typename EdgeType>
+class GraphInterface {
+ public:
+  // Edge iterator interface.
+  class EdgeIteratorIf {
+   public:
+    virtual ~EdgeIteratorIf() = default;
+
+    // Advances to the next edge.
+    //
+    // Returns:
+    // - OK if successfully advanced to the next edge.
+    // - RESOURCE_EXHAUSTED_ERROR if there is no more edge to advance to.
+    // - Any other errors from the underlying implementation.
+    virtual libtextclassifier3::Status Advance() = 0;
+
+    // Gets the current edge.
+    //
+    // REQUIRES: preceding Advance() succeeded.
+    virtual const EdgeType& Get() const = 0;
+  };
+
+  virtual ~GraphInterface() = default;
+
+  virtual int GetNumNodes() const = 0;
+
+  // Returns an iterator to the (out) edges of the given node.
+  //
+  // Returns:
+  // - On success, a non-null unique pointer of EdgeIteratorIf. If there is no
+  //   edge for a valid node_id, then it should still return a valid iterator
+  //   with no edge to advance to.
+  // - INVALID_ARGUMENT_ERROR if node_id is invalid.
+  // - Any other errors from the underlying implementation.
+  virtual libtextclassifier3::StatusOr<std::unique_ptr<EdgeIteratorIf>>
+  GetEdgesIterator(int node_id) const = 0;
+
+  // Add GetInEdgesIterator if needed.
+};
+
+}  // namespace graph
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_GRAPH_GRAPH_INTERFACE_H_
diff --git a/icing/graph/simple-graph.h b/icing/graph/simple-graph.h
new file mode 100644
index 0000000..13088b3
--- /dev/null
+++ b/icing/graph/simple-graph.h
@@ -0,0 +1,107 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef ICING_GRAPH_SIMPLE_GRAPH_H_
+#define ICING_GRAPH_SIMPLE_GRAPH_H_
+
+#include <memory>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/absl_ports/canonical_errors.h"
+#include "icing/graph/graph-interface.h"
+
+namespace icing {
+namespace lib {
+
+namespace graph {
+
+// A simple in-memory graph data structure without edge weights.
+// - Node ids are from 0 to GetNumNodes() - 1.
+// - Edges only contain the connected node ids, and are stored in adjacent
+//   lists in memory.
+class SimpleGraph : public GraphInterface<int> {
+ public:
+  // Builder class for SimpleGraph.
+  class Builder {
+   public:
+    explicit Builder(int num_nodes) : out_(num_nodes) {}
+
+    // Builds the graph. It is undefined behavior to use the builder after this
+    // call.
+    SimpleGraph Build() { return SimpleGraph(std::move(out_)); }
+
+    // Adds an edge from node u to node v.
+    //
+    // REQUIRES: 0 <= u, v < num_nodes.
+    Builder& AddEdge(int u, int v) {
+      out_[u].insert(v);
+      return *this;
+    }
+
+   private:
+    std::vector<std::unordered_set<int>> out_;
+  };
+
+  int GetNumNodes() const override { return out_edges_.size(); }
+
+  libtextclassifier3::StatusOr<std::unique_ptr<EdgeIteratorIf>>
+  GetEdgesIterator(int node_id) const override {
+    if (node_id < 0 || node_id >= GetNumNodes()) {
+      return absl_ports::InvalidArgumentError("Invalid node id.");
+    }
+    return std::make_unique<EdgeIterator>(out_edges_[node_id].cbegin(),
+                                          out_edges_[node_id].size());
+  }
+
+ private:
+  class EdgeIterator : public EdgeIteratorIf {
+   public:
+    explicit EdgeIterator(std::unordered_set<int>::const_iterator it, int len)
+        : it_(std::move(it)), len_(len), num_advanced_(0) {}
+
+    libtextclassifier3::Status Advance() override {
+      if (num_advanced_ >= len_) {
+        return absl_ports::OutOfRangeError("No more edges to advance to.");
+      }
+      if (num_advanced_ != 0) {
+        ++it_;
+      }
+      ++num_advanced_;
+      return libtextclassifier3::Status::OK;
+    }
+
+    const int& Get() const override { return *it_; }
+
+   private:
+    std::unordered_set<int>::const_iterator it_;
+    int len_;
+    int num_advanced_;
+  };
+
+  explicit SimpleGraph(std::vector<std::unordered_set<int>>&& out_edges)
+      : out_edges_(std::move(out_edges)) {}
+
+  std::vector<std::unordered_set<int>> out_edges_;
+};
+
+}  // namespace graph
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_GRAPH_SIMPLE_GRAPH_H_
diff --git a/icing/icing-search-engine.cc b/icing/icing-search-engine.cc
index 0f85590..6b6d5db 100644
--- a/icing/icing-search-engine.cc
+++ b/icing/icing-search-engine.cc
@@ -18,7 +18,9 @@
 #include <cstddef>
 #include <cstdint>
 #include <functional>
+#include <limits>
 #include <memory>
+#include <optional>
 #include <string>
 #include <string_view>
 #include <unordered_map>
@@ -51,6 +53,8 @@
 #include "icing/index/term-indexing-handler.h"
 #include "icing/index/term-metadata.h"
 #include "icing/jni/jni-cache.h"
+#include "icing/join/delete-propagation-handler.h"
+#include "icing/join/document-dependency-processor.h"
 #include "icing/join/join-children-fetcher.h"
 #include "icing/join/join-processor.h"
 #include "icing/join/qualified-id-join-index-impl-v2.h"
@@ -105,6 +109,7 @@
 #include "icing/util/data-loss.h"
 #include "icing/util/icu-data-file-helper.h"
 #include "icing/util/logging.h"
+#include "icing/util/simple-task-scheduler.h"
 #include "icing/util/status-macros.h"
 #include "icing/util/status-util.h"
 #include "icing/util/tokenized-document.h"
@@ -118,6 +123,8 @@
 
 using ::icing::lib::status_util::TransformStatus;
 
+constexpr SimpleTaskScheduler::TaskId kHandleExpiredDocumentsTaskId = 1;
+
 constexpr std::string_view kDocumentSubfolderName = "document_dir";
 constexpr std::string_view kBlobSubfolderName = "blob_dir";
 constexpr std::string_view kIndexSubfolderName = "index_dir";
@@ -192,17 +199,17 @@
          result_grouping.entry_groupings()) {
       const std::string& name_space = entry.namespace_();
       const std::string& schema = entry.schema();
-      auto entry_id_or = document_store->GetResultGroupingEntryId(
-          result_grouping_type, name_space, schema);
-      if (!entry_id_or.ok()) {
+      std::optional<int32_t> entry_id =
+          document_store->GetResultGroupingEntryId(result_grouping_type,
+                                                   name_space, schema);
+      if (!entry_id.has_value()) {
         continue;
       }
-      int32_t entry_id = entry_id_or.ValueOrDie();
-      if (unique_entry_ids.find(entry_id) != unique_entry_ids.end()) {
+      if (unique_entry_ids.find(*entry_id) != unique_entry_ids.end()) {
         return absl_ports::InvalidArgumentError(
             "Entry Ids must be unique across result groups.");
       }
-      unique_entry_ids.insert(entry_id);
+      unique_entry_ids.insert(*entry_id);
     }
   }
   return libtextclassifier3::Status::OK;
@@ -384,21 +391,6 @@
   }
 }
 
-// Prepares the document for indexing. This includes tokenization and dependency
-// enforcement.
-libtextclassifier3::StatusOr<TokenizedDocument> PrepareDocumentForIndexing(
-    const SchemaStore* schema_store,
-    const LanguageSegmenter* language_segmenter, DocumentProto&& document) {
-  ICING_ASSIGN_OR_RETURN(
-      TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store, language_segmenter,
-                                std::move(document)));
-
-  // TODO(b/384947619): apply dependency enforcement.
-
-  return tokenized_document;
-}
-
 libtextclassifier3::Status RetrieveAndAddDocumentInfo(
     const DocumentStore* document_store, DeleteByQueryResultProto& result_proto,
     std::unordered_map<NamespaceTypePair,
@@ -508,7 +500,21 @@
                      options_.enable_repeated_field_joins(),
                      options_.enable_embedding_backup_generation(),
                      options_.enable_schema_database(),
-                     options_.release_backup_schema_file_if_overlay_present()),
+                     options_.release_backup_schema_file_if_overlay_present(),
+                     options_.enable_strict_page_byte_size_limit(),
+                     options_.enable_smaller_decompression_buffer_size(),
+                     options_.enable_eigen_embedding_scoring(),
+                     options_.enable_passing_filter_to_children(),
+                     options_.enable_proto_log_new_header_format(),
+                     options_.enable_embedding_iterator_v2(),
+                     options_.enable_reusable_decompression_buffer(),
+                     options_.enable_schema_type_id_optimization(),
+                     options_.enable_optimize_improvements(),
+                     options_.expired_document_purge_threshold_ms(),
+                     options_.enable_non_existent_qualified_id_join(),
+                     options_.enable_skip_set_schema_type_equality_check(),
+                     options_.enable_embed_query_optimization(),
+                     options_.enable_schema_definition_deduping()),
       filesystem_(std::move(filesystem)),
       icing_filesystem_(std::move(icing_filesystem)),
       clock_(std::move(clock)),
@@ -517,10 +523,16 @@
 }
 
 IcingSearchEngine::~IcingSearchEngine() {
+  // Stop all scheduled background tasks before persisting to disk.
+  DestroyTaskScheduler();
+
   if (initialized_) {
     if (PersistToDisk(PersistType::FULL).status().code() != StatusProto::OK) {
       ICING_LOG(ERROR)
           << "Error persisting to disk in IcingSearchEngine destructor";
+    } else {
+      ICING_LOG(INFO)
+          << "Done persisting to disk in IcingSearchEngine destructor";
     }
   }
 }
@@ -530,10 +542,32 @@
   // locks (reader and writer) has the chance to be interrupted during
   // switching.
   absl_ports::unique_lock l(&mutex_);
-  return InternalInitialize();
+
+  InitializeResultProto result = InitializeLocked();
+  ICING_LOG(INFO)
+      << "Initialize result: document store recovery cause = "
+      << static_cast<int>(
+             result.initialize_stats().document_store_recovery_cause())
+      << ", schema store recovery cause = "
+      << static_cast<int>(
+             result.initialize_stats().schema_store_recovery_cause())
+      << ", index restoration cause = "
+      << static_cast<int>(result.initialize_stats().index_restoration_cause())
+      << ", integer index restoration cause = "
+      << static_cast<int>(
+             result.initialize_stats().integer_index_restoration_cause())
+      << ", qualified id join index restoration cause = "
+      << static_cast<int>(result.initialize_stats()
+                              .qualified_id_join_index_restoration_cause())
+      << ", embedding index restoration cause = "
+      << static_cast<int>(
+             result.initialize_stats().embedding_index_restoration_cause());
+  result.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
+  return result;
 }
 
-void IcingSearchEngine::ResetMembers() {
+void IcingSearchEngine::ResetMembersLocked() {
   // Reset all members in the reverse order of their initialization to ensure
   // the dependencies are not violated.
   embedding_index_.reset();
@@ -566,12 +600,13 @@
   libtextclassifier3::Status status;
   if (file_exists &&
       filesystem_->PRead(marker_file_fd->get(), &network_init_attempts,
-                         sizeof(network_init_attempts), /*offset=*/0)) {
+                         sizeof(network_init_attempts),
+                         /*offset=*/0) == sizeof(network_init_attempts)) {
     host_init_attempts = GNetworkToHostL(network_init_attempts);
     if (host_init_attempts > kMaxUnsuccessfulInitAttempts) {
       // We're tried and failed to init too many times. We need to throw
       // everything out and start from scratch.
-      ResetMembers();
+      ResetMembersLocked();
       marker_file_fd.reset();
 
       // Delete the entire base directory.
@@ -614,7 +649,7 @@
   return status;
 }
 
-InitializeResultProto IcingSearchEngine::InternalInitialize() {
+InitializeResultProto IcingSearchEngine::InitializeLocked() {
   ICING_VLOG(1) << "Initializing IcingSearchEngine in dir: "
                 << options_.base_dir();
 
@@ -635,11 +670,14 @@
   }
 
   if (options_.enable_delete_propagation_from() &&
-      !options_.enable_qualified_id_join_index_v3()) {
+      (!options_.enable_qualified_id_join_index_v3() ||
+       !options_.enable_soft_index_restoration())) {
     result_status->set_code(StatusProto::INVALID_ARGUMENT);
     result_status->set_message(
-        "Delete propagation is enabled but qualified id join index v3 is not "
-        "enabled.");
+        "Delete propagation is enabled but qualified id join index v3 or soft "
+        "index restoration is not enabled.");
+    initialize_stats->set_failure_stage(
+        InitializeStatsProto::FailureStage::OPTIONS_VALIDATION);
     return result_proto;
   }
 
@@ -653,6 +691,24 @@
       status = absl_ports::InternalError("Failed to delete init marker file!");
     } else {
       initialized_ = true;
+
+      // Set needs_persist_type if anything was rebuilt.
+      if (initialize_stats->schema_store_recovery_cause() !=
+              InitializeStatsProto::NONE ||
+          initialize_stats->document_store_data_status() !=
+              InitializeStatsProto::NO_DATA_LOSS ||
+          initialize_stats->document_store_recovery_cause() !=
+              InitializeStatsProto::NONE ||
+          initialize_stats->index_restoration_cause() !=
+              InitializeStatsProto::NONE ||
+          initialize_stats->integer_index_restoration_cause() !=
+              InitializeStatsProto::NONE ||
+          initialize_stats->qualified_id_join_index_restoration_cause() !=
+              InitializeStatsProto::NONE ||
+          initialize_stats->embedding_index_restoration_cause() !=
+              InitializeStatsProto::NONE) {
+        result_proto.set_needs_persist_type(PersistType::RECOVERY_PROOF);
+      }
     }
   }
   TransformStatus(status, result_status);
@@ -665,6 +721,8 @@
   ICING_RETURN_ERROR_IF_NULL(initialize_stats);
   // Make sure the base directory exists
   if (!filesystem_->CreateDirectoryRecursively(options_.base_dir().c_str())) {
+    initialize_stats->set_failure_stage(
+        InitializeStatsProto::FailureStage::BASE_DIRECTORY_CREATION);
     return absl_ports::InternalError(absl_ports::StrCat(
         "Could not create directory: ", options_.base_dir()));
   }
@@ -673,6 +731,8 @@
   // max number of init attempts.
   libtextclassifier3::Status status = CheckInitMarkerFile(initialize_stats);
   if (!status.ok() && !absl_ports::IsDataLoss(status)) {
+    initialize_stats->set_failure_stage(
+        InitializeStatsProto::FailureStage::INIT_MARKER_FILE);
     return status;
   }
 
@@ -686,10 +746,24 @@
   // Read version file, determine the state change and rebuild derived files if
   // needed.
   const std::string index_dir = MakeIndexDirectoryPath(options_.base_dir());
-  ICING_ASSIGN_OR_RETURN(
-      IcingSearchEngineVersionProto stored_version_proto,
-      version_util::ReadVersion(
-          *filesystem_, /*version_file_dir=*/options_.base_dir(), index_dir));
+  auto stored_version_proto_or = version_util::ReadVersion(
+      *filesystem_, /*version_file_dir=*/options_.base_dir(), index_dir);
+  if (!stored_version_proto_or.ok()) {
+    initialize_stats->set_failure_stage(
+        InitializeStatsProto::FailureStage::READ_VERSION_FILE);
+    ICING_LOG(ERROR) << "Failed to read version file. Error: "
+                     << stored_version_proto_or.status().error_code()
+                     << ", message: "
+                     << stored_version_proto_or.status().error_message();
+    return std::move(stored_version_proto_or).status();
+  }
+  IcingSearchEngineVersionProto stored_version_proto =
+      std::move(stored_version_proto_or).ValueOrDie();
+  ICING_LOG(INFO)
+      << "Successfully read version proto from existing dataset. Version: "
+      << stored_version_proto.version()
+      << ", max_version: " << stored_version_proto.max_version();
+
   version_util::VersionInfo stored_version_info =
       version_util::GetVersionInfoFromProto(stored_version_proto);
   version_util::StateChange version_state_change =
@@ -707,10 +781,15 @@
   bool perform_schema_database_migration =
       version_util::SchemaDatabaseMigrationRequired(stored_version_proto) &&
       options_.enable_schema_database();
-  ICING_RETURN_IF_ERROR(SchemaStore::MigrateSchema(
+  auto migrate_status = SchemaStore::MigrateSchema(
       filesystem_.get(), MakeSchemaDirectoryPath(options_.base_dir()),
       version_state_change, version_util::kVersion,
-      perform_schema_database_migration));
+      perform_schema_database_migration);
+  if (!migrate_status.ok()) {
+    initialize_stats->set_failure_stage(
+        InitializeStatsProto::FailureStage::MIGRATE_SCHEMA);
+    return migrate_status;
+  }
 
   // Step 2: Discard derived files that need to be rebuilt
   derived_file_util::DerivedFilesRebuildInfo required_derived_files_rebuild =
@@ -719,17 +798,32 @@
   if (existing_marker_proto != nullptr) {
     required_derived_files_rebuild.RebuildAll();
   }
-  ICING_RETURN_IF_ERROR(DiscardDerivedFiles(required_derived_files_rebuild));
+  auto discard_status = DiscardDerivedFiles(required_derived_files_rebuild);
+  if (!discard_status.ok()) {
+    initialize_stats->set_failure_stage(
+        InitializeStatsProto::FailureStage::DISCARD_DERIVED_FILES);
+    return discard_status;
+  }
 
   // Step 3: update version files. We need to update both the V1 and V2
   // version files.
-  ICING_RETURN_IF_ERROR(version_util::WriteV1Version(
+  auto write_version_status = version_util::WriteV1Version(
       *filesystem_, /*version_file_dir=*/options_.base_dir(),
-      version_util::GetVersionInfoFromProto(current_version_proto)));
-  ICING_RETURN_IF_ERROR(version_util::WriteV2Version(
+      version_util::GetVersionInfoFromProto(current_version_proto));
+  if (!write_version_status.ok()) {
+    initialize_stats->set_failure_stage(
+        InitializeStatsProto::FailureStage::WRITE_VERSION_FILE);
+    return write_version_status;
+  }
+  write_version_status = version_util::WriteV2Version(
       *filesystem_, /*version_file_dir=*/options_.base_dir(),
       std::make_unique<IcingSearchEngineVersionProto>(
-          std::move(current_version_proto))));
+          std::move(current_version_proto)));
+  if (!write_version_status.ok()) {
+    initialize_stats->set_failure_stage(
+        InitializeStatsProto::FailureStage::WRITE_VERSION_FILE);
+    return write_version_status;
+  }
 
   ICING_RETURN_IF_ERROR(InitializeSchemaStore(initialize_stats));
 
@@ -748,13 +842,35 @@
   // TODO(b/156383798) : Resolve how to specify the locale.
   language_segmenter_factory::SegmenterOptions segmenter_options(
       ULOC_US, jni_cache_.get(), enable_icu);
-  TC3_ASSIGN_OR_RETURN(language_segmenter_, language_segmenter_factory::Create(
-                                                std::move(segmenter_options)));
+  StatusProto* icu_segmenter_creation_status = nullptr;
+  if (enable_icu) {
+    icu_segmenter_creation_status =
+        initialize_stats->mutable_icu_segmenter_creation_status();
+  }
+  auto language_segmenter_or = language_segmenter_factory::Create(
+      std::move(segmenter_options), icu_segmenter_creation_status);
+  if (!language_segmenter_or.ok()) {
+    initialize_stats->set_failure_stage(
+        InitializeStatsProto::FailureStage::LANGUAGE_SEGMENTER_CREATION);
+    return std::move(language_segmenter_or).status();
+  }
+  language_segmenter_ = std::move(language_segmenter_or).ValueOrDie();
 
   NormalizerOptions normalizer_options(
       /*max_term_byte_size=*/options_.max_token_length(), enable_icu);
-  TC3_ASSIGN_OR_RETURN(normalizer_,
-                       normalizer_factory::Create(normalizer_options));
+  StatusProto* icu_normalizer_creation_status = nullptr;
+  if (enable_icu) {
+    icu_normalizer_creation_status =
+        initialize_stats->mutable_icu_normalizer_creation_status();
+  }
+  auto normalizer_or = normalizer_factory::Create(
+      normalizer_options, icu_normalizer_creation_status);
+  if (!normalizer_or.ok()) {
+    initialize_stats->set_failure_stage(
+        InitializeStatsProto::FailureStage::NORMALIZER_CREATION);
+    return std::move(normalizer_or).status();
+  }
+  normalizer_ = std::move(normalizer_or).ValueOrDie();
 
   libtextclassifier3::Status index_init_status;
   if (absl_ports::IsNotFound(schema_store_->GetSchema().status())) {
@@ -782,6 +898,8 @@
              .ok() ||
         !EmbeddingIndex::Discard(*filesystem_, embedding_index_dir).ok() ||
         !filesystem_->DeleteDirectoryRecursively(blob_store_dir.c_str())) {
+      initialize_stats->set_failure_stage(
+          InitializeStatsProto::FailureStage::DISCARD_COMPONENTS);
       return absl_ports::InternalError(absl_ports::StrCat(
           "Could not delete directories: ", index_dir, ", ", integer_index_dir,
           ", ", qualified_id_join_index_dir, ", ", embedding_index_dir, ", ",
@@ -790,9 +908,15 @@
 
     // Initialize (empty) blob store.
     if (options_.enable_blob_store()) {
-      ICING_RETURN_IF_ERROR(
+      auto blob_store_init_status =
           InitializeBlobStore(options_.orphan_blob_time_to_live_ms(),
-                              options_.blob_store_compression_level()));
+                              options_.blob_store_compression_level(),
+                              options_.blob_store_compression_mem_level());
+      if (!blob_store_init_status.ok()) {
+        initialize_stats->set_failure_stage(
+            InitializeStatsProto::FailureStage::BLOB_STORE_INSTANTIATION);
+        return blob_store_init_status;
+      }
     }
 
     // Initialize (empty) document store.
@@ -825,9 +949,15 @@
     // We just need to re-initialize each component here.
 
     if (options_.enable_blob_store()) {
-      ICING_RETURN_IF_ERROR(
+      auto blob_store_init_status =
           InitializeBlobStore(options_.orphan_blob_time_to_live_ms(),
-                              options_.blob_store_compression_level()));
+                              options_.blob_store_compression_level(),
+                              options_.blob_store_compression_mem_level());
+      if (!blob_store_init_status.ok()) {
+        initialize_stats->set_failure_stage(
+            InitializeStatsProto::FailureStage::BLOB_STORE_INSTANTIATION);
+        return blob_store_init_status;
+      }
     }
 
     // Initialize document store. This also rebuilds all derived files in the
@@ -868,9 +998,15 @@
 
     // Initialize blob store.
     if (options_.enable_blob_store()) {
-      ICING_RETURN_IF_ERROR(
+      auto blob_store_init_status =
           InitializeBlobStore(options_.orphan_blob_time_to_live_ms(),
-                              options_.blob_store_compression_level()));
+                              options_.blob_store_compression_level(),
+                              options_.blob_store_compression_mem_level());
+      if (!blob_store_init_status.ok()) {
+        initialize_stats->set_failure_stage(
+            InitializeStatsProto::FailureStage::BLOB_STORE_INSTANTIATION);
+        return blob_store_init_status;
+      }
     }
 
     // Initialize document store. This also rebuilds all derived files in the
@@ -918,10 +1054,40 @@
     }
   }
 
+  if (options_.enable_background_task_scheduler() &&
+      task_scheduler_ == nullptr) {
+    // Initialize the task scheduler.
+    //
+    // Note: InitializeMembers may be called by ResetLocked() when put API
+    //   fails. In that case, we can still use the existing task_scheduler_
+    //   and no need to re-initialize it.
+    task_scheduler_ = SimpleTaskScheduler::Create(*clock_);
+  }
+
   if (status.ok()) {
     status = index_init_status;
   }
 
+  // Call HandleExpiredDocumentsLocked() to handle documents that expire during
+  // Icing was off. This function will reschedule another task with the next
+  // expiration timestamp if task_scheduler_ is not null (i.e.
+  // options_.enable_background_task_scheduler() is true).
+  if ((status.ok() || absl_ports::IsDataLoss(status)) &&
+      options_.enable_delete_propagation_from()) {
+    // Call HandleExpiredDocumentsLocked() to handle documents that expire
+    // during Icing was off. This function will reschedule another task with the
+    // next expiration timestamp and activate the task.
+    HandleExpiredDocumentsResultProto result = HandleExpiredDocumentsLocked();
+    if (result.status().code() != StatusProto::OK) {
+      ICING_LOG(ERROR) << "Failed to handle expired documents during "
+                          "initialization: "
+                       << result.status().message();
+      return absl_ports::InternalError(result.status().message());
+    }
+    initialize_stats->set_next_expiration_timestamp_ms(
+        result.next_expiration_timestamp_ms());
+  }
+
   result_state_manager_ = std::make_unique<ResultStateManager>(
       performance_configuration_.max_num_total_hits, *document_store_);
 
@@ -936,13 +1102,21 @@
       MakeSchemaDirectoryPath(options_.base_dir());
   // Make sure the sub-directory exists
   if (!filesystem_->CreateDirectoryRecursively(schema_store_dir.c_str())) {
+    initialize_stats->set_failure_stage(
+        InitializeStatsProto::FailureStage::SCHEMA_STORE_DIRECTORY_CREATION);
     return absl_ports::InternalError(
         absl_ports::StrCat("Could not create directory: ", schema_store_dir));
   }
-  ICING_ASSIGN_OR_RETURN(
-      schema_store_,
+
+  auto schema_store_or =
       SchemaStore::Create(filesystem_.get(), schema_store_dir, clock_.get(),
-                          &feature_flags_, initialize_stats));
+                          &feature_flags_, initialize_stats);
+  if (!schema_store_or.ok()) {
+    initialize_stats->set_failure_stage(
+        InitializeStatsProto::FailureStage::SCHEMA_STORE_INSTANTIATION);
+    return schema_store_or.status();
+  }
+  schema_store_ = std::move(schema_store_or).ValueOrDie();
 
   return libtextclassifier3::Status::OK;
 }
@@ -956,22 +1130,33 @@
       MakeDocumentDirectoryPath(options_.base_dir());
   // Make sure the sub-directory exists
   if (!filesystem_->CreateDirectoryRecursively(document_dir.c_str())) {
+    initialize_stats->set_failure_stage(
+        InitializeStatsProto::FailureStage::DOCUMENT_STORE_DIRECTORY_CREATION);
     return absl_ports::InternalError(
         absl_ports::StrCat("Could not create directory: ", document_dir));
   }
-  ICING_ASSIGN_OR_RETURN(
-      DocumentStore::CreateResult create_result,
-      DocumentStore::Create(
-          filesystem_.get(), document_dir, clock_.get(), schema_store_.get(),
-          &feature_flags_, force_recovery_and_revalidate_documents,
-          /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
-          options_.compression_level(), initialize_stats));
+
+  auto create_result_or = DocumentStore::Create(
+      filesystem_.get(), document_dir, clock_.get(), schema_store_.get(),
+      &feature_flags_, force_recovery_and_revalidate_documents,
+      /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
+      options_.compression_level(), options_.compression_threshold_bytes(),
+      options_.compression_mem_level(), initialize_stats);
+  if (!create_result_or.ok()) {
+    initialize_stats->set_failure_stage(
+        InitializeStatsProto::FailureStage::DOCUMENT_STORE_INSTANTIATION);
+    return std::move(create_result_or).status();
+  }
+  DocumentStore::CreateResult create_result =
+      std::move(create_result_or).ValueOrDie();
+
   document_store_ = std::move(create_result.document_store);
   return create_result.derived_files_regenerated;
 }
 
 libtextclassifier3::Status IcingSearchEngine::InitializeBlobStore(
-    int32_t orphan_blob_time_to_live_ms, int32_t blob_store_compression_level) {
+    int32_t orphan_blob_time_to_live_ms, int32_t blob_store_compression_level,
+    int32_t blob_store_compression_mem_level) {
   std::string blob_dir = MakeBlobDirectoryPath(options_.base_dir());
   // Make sure the sub-directory exists
   if (!filesystem_->CreateDirectoryRecursively(blob_dir.c_str())) {
@@ -984,7 +1169,8 @@
       BlobStore::Create(filesystem_.get(), blob_dir, clock_.get(),
                         orphan_blob_time_to_live_ms,
                         blob_store_compression_level,
-                        options_.manage_blob_files()));
+                        blob_store_compression_mem_level,
+                        options_.manage_blob_files(), &feature_flags_));
   blob_store_ = std::make_unique<BlobStore>(std::move(blob_store_or));
   return libtextclassifier3::Status::OK;
 }
@@ -997,6 +1183,8 @@
   const std::string index_dir = MakeIndexDirectoryPath(options_.base_dir());
   // Make sure the sub-directory exists
   if (!filesystem_->CreateDirectoryRecursively(index_dir.c_str())) {
+    initialize_stats->set_failure_stage(
+        InitializeStatsProto::FailureStage::TERM_INDEX_DIRECTORY);
     return absl_ports::InternalError(
         absl_ports::StrCat("Could not create directory: ", index_dir));
   }
@@ -1006,11 +1194,13 @@
 
   // Term index
   InitializeStatsProto::RecoveryCause index_recovery_cause;
-  auto index_or =
-      Index::Create(index_options, filesystem_.get(), icing_filesystem_.get());
+  auto index_or = Index::Create(index_options, filesystem_.get(),
+                                icing_filesystem_.get(), &feature_flags_);
   if (!index_or.ok()) {
     if (!filesystem_->DeleteDirectoryRecursively(index_dir.c_str()) ||
         !filesystem_->CreateDirectoryRecursively(index_dir.c_str())) {
+      initialize_stats->set_failure_stage(
+          InitializeStatsProto::FailureStage::TERM_INDEX_DIRECTORY);
       return absl_ports::InternalError(
           absl_ports::StrCat("Could not recreate directory: ", index_dir));
     }
@@ -1018,9 +1208,14 @@
     index_recovery_cause = InitializeStatsProto::IO_ERROR;
 
     // Try recreating it from scratch and re-indexing everything.
-    ICING_ASSIGN_OR_RETURN(index_,
-                           Index::Create(index_options, filesystem_.get(),
-                                         icing_filesystem_.get()));
+    index_or = Index::Create(index_options, filesystem_.get(),
+                             icing_filesystem_.get(), &feature_flags_);
+    if (!index_or.ok()) {
+      initialize_stats->set_failure_stage(
+          InitializeStatsProto::FailureStage::TERM_INDEX_INSTANTIATION);
+      return std::move(index_or).status();
+    }
+    index_ = std::move(index_or).ValueOrDie();
   } else {
     // Index was created fine.
     index_ = std::move(index_or).ValueOrDie();
@@ -1038,17 +1233,27 @@
                            options_.integer_index_bucket_split_threshold(),
                            options_.pre_mapping_fbv());
   if (!integer_index_or.ok()) {
-    ICING_RETURN_IF_ERROR(
-        IntegerIndex::Discard(*filesystem_, integer_index_dir));
+    auto discard_status =
+        IntegerIndex::Discard(*filesystem_, integer_index_dir);
+    if (!discard_status.ok()) {
+      initialize_stats->set_failure_stage(
+          InitializeStatsProto::FailureStage::INTEGER_INDEX_DIRECTORY);
+      return discard_status;
+    }
 
     integer_index_recovery_cause = InitializeStatsProto::IO_ERROR;
 
     // Try recreating it from scratch and re-indexing everything.
-    ICING_ASSIGN_OR_RETURN(
-        integer_index_,
+    integer_index_or =
         IntegerIndex::Create(*filesystem_, std::move(integer_index_dir),
                              options_.integer_index_bucket_split_threshold(),
-                             options_.pre_mapping_fbv()));
+                             options_.pre_mapping_fbv());
+    if (!integer_index_or.ok()) {
+      initialize_stats->set_failure_stage(
+          InitializeStatsProto::FailureStage::INTEGER_INDEX_INSTANTIATION);
+      return std::move(integer_index_or).status();
+    }
+    integer_index_ = std::move(integer_index_or).ValueOrDie();
   } else {
     // Integer index was created fine.
     integer_index_ = std::move(integer_index_or).ValueOrDie();
@@ -1067,14 +1272,25 @@
     // V2 qualified id join index depends on document store derived files, so we
     // have to rebuild it from scratch if
     // document_store_derived_files_regenerated is true.
-    ICING_RETURN_IF_ERROR(QualifiedIdJoinIndex::Discard(
-        *filesystem_, qualified_id_join_index_dir));
+    auto discard_status = QualifiedIdJoinIndex::Discard(
+        *filesystem_, qualified_id_join_index_dir);
+    if (!discard_status.ok()) {
+      initialize_stats->set_failure_stage(
+          InitializeStatsProto::FailureStage::
+              QUALIFIED_ID_JOIN_INDEX_DIRECTORY);
+      return discard_status;
+    }
 
-    ICING_ASSIGN_OR_RETURN(
-        qualified_id_join_index_,
-        CreateQualifiedIdJoinIndex(*filesystem_,
-                                   std::move(qualified_id_join_index_dir),
-                                   options_, feature_flags_));
+    auto qualified_id_join_index_or = CreateQualifiedIdJoinIndex(
+        *filesystem_, qualified_id_join_index_dir, options_, feature_flags_);
+    if (!qualified_id_join_index_or.ok()) {
+      initialize_stats->set_failure_stage(
+          InitializeStatsProto::FailureStage::
+              QUALIFIED_ID_JOIN_INDEX_INSTANTIATION);
+      return std::move(qualified_id_join_index_or).status();
+    }
+    qualified_id_join_index_ =
+        std::move(qualified_id_join_index_or).ValueOrDie();
 
     qualified_id_join_index_recovery_cause =
         InitializeStatsProto::DEPENDENCIES_CHANGED;
@@ -1082,17 +1298,29 @@
     auto qualified_id_join_index_or = CreateQualifiedIdJoinIndex(
         *filesystem_, qualified_id_join_index_dir, options_, feature_flags_);
     if (!qualified_id_join_index_or.ok()) {
-      ICING_RETURN_IF_ERROR(QualifiedIdJoinIndex::Discard(
-          *filesystem_, qualified_id_join_index_dir));
+      auto discard_status = QualifiedIdJoinIndex::Discard(
+          *filesystem_, qualified_id_join_index_dir);
+      if (!discard_status.ok()) {
+        initialize_stats->set_failure_stage(
+            InitializeStatsProto::FailureStage::
+                QUALIFIED_ID_JOIN_INDEX_DIRECTORY);
+        return discard_status;
+      }
 
       qualified_id_join_index_recovery_cause = InitializeStatsProto::IO_ERROR;
 
       // Try recreating it from scratch and rebuild everything.
-      ICING_ASSIGN_OR_RETURN(
-          qualified_id_join_index_,
-          CreateQualifiedIdJoinIndex(*filesystem_,
-                                     std::move(qualified_id_join_index_dir),
-                                     options_, feature_flags_));
+      qualified_id_join_index_or = CreateQualifiedIdJoinIndex(
+          *filesystem_, std::move(qualified_id_join_index_dir), options_,
+          feature_flags_);
+      if (!qualified_id_join_index_or.ok()) {
+        initialize_stats->set_failure_stage(
+            InitializeStatsProto::FailureStage::
+                QUALIFIED_ID_JOIN_INDEX_INSTANTIATION);
+        return std::move(qualified_id_join_index_or).status();
+      }
+      qualified_id_join_index_ =
+          std::move(qualified_id_join_index_or).ValueOrDie();
     } else {
       // Qualified id join index was created fine.
       qualified_id_join_index_ =
@@ -1109,17 +1337,28 @@
       MakeEmbeddingIndexWorkingPath(options_.base_dir());
   InitializeStatsProto::RecoveryCause embedding_index_recovery_cause;
   auto embedding_index_or = EmbeddingIndex::Create(
-      filesystem_.get(), embedding_dir, clock_.get(), &feature_flags_);
+      filesystem_.get(), embedding_dir, clock_.get(), &feature_flags_,
+      options_.embedding_index_num_shards());
   if (!embedding_index_or.ok()) {
-    ICING_RETURN_IF_ERROR(EmbeddingIndex::Discard(*filesystem_, embedding_dir));
+    auto discard_status = EmbeddingIndex::Discard(*filesystem_, embedding_dir);
+    if (!discard_status.ok()) {
+      initialize_stats->set_failure_stage(
+          InitializeStatsProto::FailureStage::EMBEDDING_INDEX_DIRECTORY);
+      return discard_status;
+    }
 
     embedding_index_recovery_cause = InitializeStatsProto::IO_ERROR;
 
     // Try recreating it from scratch and re-indexing everything.
-    ICING_ASSIGN_OR_RETURN(
-        embedding_index_,
-        EmbeddingIndex::Create(filesystem_.get(), embedding_dir, clock_.get(),
-                               &feature_flags_));
+    embedding_index_or = EmbeddingIndex::Create(
+        filesystem_.get(), embedding_dir, clock_.get(), &feature_flags_,
+        options_.embedding_index_num_shards());
+    if (!embedding_index_or.ok()) {
+      initialize_stats->set_failure_stage(
+          InitializeStatsProto::FailureStage::EMBEDDING_INDEX_INSTANTIATION);
+      return std::move(embedding_index_or).status();
+    }
+    embedding_index_ = std::move(embedding_index_or).ValueOrDie();
   } else {
     // Embedding index was created fine.
     embedding_index_ = std::move(embedding_index_or).ValueOrDie();
@@ -1156,6 +1395,11 @@
           embedding_index_recovery_cause);
     }
   }
+  if (!restore_result.status.ok() &&
+      !absl_ports::IsDataLoss(restore_result.status)) {
+    initialize_stats->set_failure_stage(
+        InitializeStatsProto::FailureStage::RESTORE_INDEX);
+  }
   return restore_result.status;
 }
 
@@ -1170,8 +1414,10 @@
   *set_schema_request.mutable_schema() = std::move(new_schema);
   set_schema_request.set_ignore_errors_and_delete_documents(
       ignore_errors_and_delete_documents);
-
-  return SetSchema(std::move(set_schema_request));
+  SetSchemaResultProto result_proto = SetSchema(std::move(set_schema_request));
+  result_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
+  return result_proto;
 }
 
 SetSchemaResultProto IcingSearchEngine::SetSchema(
@@ -1179,12 +1425,15 @@
   ICING_VLOG(1) << "Setting new Schema";
 
   SetSchemaResultProto result_proto;
+  SetSchemaStatsProto* set_schema_stats =
+      result_proto.mutable_set_schema_stats();
   StatusProto* result_status = result_proto.mutable_status();
 
   absl_ports::unique_lock l(&mutex_);
-  ScopedTimer timer(clock_->GetNewTimer(), [&result_proto](int64_t t) {
-    result_proto.set_latency_ms(t);
-  });
+  ScopedTimer overall_timer(clock_->GetNewTimer(),
+                            [&set_schema_stats](int64_t t) {
+                              set_schema_stats->set_overall_latency_ms(t);
+                            });
   if (!initialized_) {
     result_status->set_code(StatusProto::FAILED_PRECONDITION);
     result_status->set_message("IcingSearchEngine has not been initialized!");
@@ -1261,68 +1510,134 @@
   //   stores schema type id (+ joinable property path) as a key to group join
   //   data.
   bool join_incompatible =
-      !set_schema_result.schema_types_join_incompatible_by_name.empty() ||
-      !set_schema_result.old_schema_type_ids_changed.empty();
+      !set_schema_result.schema_types_join_incompatible_by_name.empty();
+  if (!options_.enable_qualified_id_join_index_v3()) {
+    join_incompatible |= !set_schema_result.old_schema_type_ids_changed.empty();
+  }
   for (const std::string& join_incompatible_type :
        set_schema_result.schema_types_join_incompatible_by_name) {
     result_proto.add_join_incompatible_changed_schema_types(
         std::move(join_incompatible_type));
   }
 
+  set_schema_stats->set_schema_store_set_schema_latency_ms(
+      overall_timer.timer().GetElapsedMilliseconds());
+
   libtextclassifier3::Status status;
   if (set_schema_result.success) {
+    // - No need to manually persist the schema file since
+    //   SchemaStore::SetSchema already handles schema store file persistence
+    //   internally.
+    // - We just need to detect ground truth and derived files changes from the
+    //   document store and indices.
+    bool needs_flush_ground_truth = false;
+    bool needs_flush_derived_files = false;
+
+    // Update document store if necessary.
+    std::optional<DocumentStore::UpdateSchemaStoreResult> update_result;
     if (lost_previous_schema) {
+      ScopedTimer update_schema_store_timer(
+          clock_->GetNewTimer(), [&set_schema_stats](int64_t t) {
+            set_schema_stats->set_document_store_update_schema_latency_ms(t);
+          });
       // No previous schema to calculate a diff against. We have to go through
       // and revalidate all the Documents in the DocumentStore
-      status = document_store_->UpdateSchemaStore(schema_store_.get());
-      if (!status.ok()) {
-        TransformStatus(status, result_status);
+      auto update_result_or =
+          document_store_->UpdateSchemaStore(schema_store_.get());
+      if (!update_result_or.ok()) {
+        TransformStatus(update_result_or.status(), result_status);
         return result_proto;
       }
+      update_result =
+          std::make_optional(std::move(update_result_or).ValueOrDie());
     } else if (!set_schema_result.old_schema_type_ids_changed.empty() ||
                !set_schema_result.schema_types_incompatible_by_id.empty() ||
                !set_schema_result.schema_types_deleted_by_id.empty()) {
-      status = document_store_->OptimizedUpdateSchemaStore(schema_store_.get(),
-                                                           set_schema_result);
-      if (!status.ok()) {
-        TransformStatus(status, result_status);
+      ScopedTimer optimized_update_schema_store_timer(
+          clock_->GetNewTimer(), [&set_schema_stats](int64_t t) {
+            set_schema_stats
+                ->set_document_store_optimized_update_schema_latency_ms(t);
+          });
+      auto update_result_or = document_store_->OptimizedUpdateSchemaStore(
+          schema_store_.get(), set_schema_result);
+      if (!update_result_or.ok()) {
+        TransformStatus(update_result_or.status(), result_status);
         return result_proto;
       }
+      update_result =
+          std::make_optional(std::move(update_result_or).ValueOrDie());
+    }
+    if (update_result.has_value()) {
+      result_proto.set_deleted_document_count(
+          update_result->deleted_document_count);
+      needs_flush_ground_truth |= (update_result->deleted_document_count > 0);
+      needs_flush_derived_files |= update_result->derived_files_changed;
     }
 
-    if (lost_previous_schema || index_incompatible) {
-      // Clears search indices
-      status = ClearSearchIndices();
-      if (!status.ok()) {
-        TransformStatus(status, result_status);
-        return result_proto;
+    {
+      // Restore indices if needed.
+      ScopedTimer index_restoration_timer(
+          clock_->GetNewTimer(), [&set_schema_stats](int64_t t) {
+            set_schema_stats->set_index_restoration_latency_ms(t);
+          });
+
+      if (lost_previous_schema || index_incompatible) {
+        // Clears search indices
+        status = ClearSearchIndices();
+        if (!status.ok()) {
+          TransformStatus(status, result_status);
+          return result_proto;
+        }
       }
-    }
 
-    if (lost_previous_schema || join_incompatible) {
-      // Clears join indices
-      status = ClearJoinIndices();
-      if (!status.ok()) {
-        TransformStatus(status, result_status);
-        return result_proto;
+      if (lost_previous_schema || join_incompatible) {
+        // Clears join indices
+        status = ClearJoinIndices();
+        if (!status.ok()) {
+          TransformStatus(status, result_status);
+          return result_proto;
+        }
       }
-    }
 
-    if (lost_previous_schema || index_incompatible || join_incompatible) {
-      IndexRestorationResult restore_result = RestoreIndexIfNeeded();
-      // DATA_LOSS means that we have successfully re-added content to the
-      // index. Some indexed content was lost, but otherwise the index is in a
-      // valid state and can be queried.
-      if (!restore_result.status.ok() &&
-          !absl_ports::IsDataLoss(restore_result.status)) {
-        TransformStatus(status, result_status);
-        return result_proto;
+      if (lost_previous_schema || index_incompatible || join_incompatible) {
+        needs_flush_derived_files = true;
+
+        IndexRestorationResult restore_result = RestoreIndexIfNeeded();
+        result_proto.set_has_term_index_restored(
+            restore_result.has_index_restored);
+        result_proto.set_has_integer_index_restored(
+            restore_result.has_integer_index_restored);
+        result_proto.set_has_qualified_id_join_index_restored(
+            restore_result.has_qualified_id_join_index_restored);
+        result_proto.set_has_embedding_index_restored(
+            restore_result.has_embedding_index_restored);
+        // DATA_LOSS means that we have successfully re-added content to the
+        // index. Some indexed content was lost, but otherwise the index is in a
+        // valid state and can be queried.
+        if (!restore_result.status.ok() &&
+            !absl_ports::IsDataLoss(restore_result.status)) {
+          TransformStatus(status, result_status);
+          return result_proto;
+        }
       }
     }
 
     if (feature_flags_.enable_scorable_properties()) {
       if (!set_schema_result.schema_types_scorable_property_inconsistent_by_id
                .empty()) {
+        needs_flush_derived_files = true;
+
+        ScopedTimer scorable_property_cache_regeneration_timer(
+            clock_->GetNewTimer(), [&set_schema_stats](int64_t t) {
+              set_schema_stats
+                  ->set_scorable_property_cache_regeneration_latency_ms(t);
+            });
+        for (const std::string& scorable_property_incompatible_type :
+             set_schema_result
+                 .schema_types_scorable_property_inconsistent_by_name) {
+          result_proto.add_scorable_property_incompatible_changed_schema_types(
+              scorable_property_incompatible_type);
+        }
         status = document_store_->RegenerateScorablePropertyCache(
             set_schema_result
                 .schema_types_scorable_property_inconsistent_by_id);
@@ -1332,8 +1647,28 @@
         }
       }
     }
-
     result_status->set_code(StatusProto::OK);
+    if (needs_flush_derived_files) {
+      // If derived files need to be flushed, then we need RECOVERY_PROOF which:
+      // - Updates all checksums of derived files.
+      // - Flushes ground truth data.
+      //
+      // Here, it is ok to use RECOVERY_PROOF even if ground truth does not need
+      // to be flushed.
+      result_proto.set_needs_persist_type(PersistType::RECOVERY_PROOF);
+    } else if (needs_flush_ground_truth) {
+      // If derived files are unchanged but ground truth needs to be flushed,
+      // then we need LITE which only flushes ground truth data.
+      //
+      // Theoretically, it is impossible to have this case since:
+      // - The only possibility of ground truth change is incompatible document
+      //   deletion.
+      // - When deleting incompatible documents, derived files are always
+      //   changed, so it should belong to the RECOVERY_PROOF case.
+      result_proto.set_needs_persist_type(PersistType::LITE);
+    } else {
+      result_proto.set_needs_persist_type(PersistType::UNKNOWN);
+    }
   } else {
     result_status->set_code(StatusProto::FAILED_PRECONDITION);
     result_status->set_message("Schema is incompatible.");
@@ -1396,17 +1731,23 @@
   if (!initialized_) {
     result_status->set_code(StatusProto::FAILED_PRECONDITION);
     result_status->set_message("IcingSearchEngine has not been initialized!");
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return result_proto;
   }
 
   auto type_config_or = schema_store_->GetSchemaTypeConfig(schema_type);
   if (!type_config_or.ok()) {
     TransformStatus(type_config_or.status(), result_status);
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return result_proto;
   }
 
   result_status->set_code(StatusProto::OK);
   *result_proto.mutable_schema_type_config() = *(type_config_or.ValueOrDie());
+  result_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
   return result_proto;
 }
 
@@ -1414,19 +1755,52 @@
     PutDocumentRequest&& put_document_request) {
   BatchPutResultProto batch_put_result_proto;
 
-  // TODO(b/394875109): right now we lock in the Put(DocumentProto&&) for each
-  // document. We should considering just locking once for the whole batch here.
+  absl_ports::unique_lock l(&mutex_);  // Acquire lock once for the batch
+
+  if (!initialized_) {
+    // Handle not initialized case for all documents
+    for (const DocumentProto& document_proto :
+         put_document_request.documents()) {
+      PutResultProto* put_result =
+          batch_put_result_proto.mutable_put_result_protos()->Add();
+      put_result->set_uri(document_proto.uri());
+      put_result->mutable_status()->set_code(StatusProto::FAILED_PRECONDITION);
+    }
+    batch_put_result_proto.mutable_status()->set_message(
+        "IcingSearchEngine has not been initialized!");
+    batch_put_result_proto.mutable_status()->set_code(
+        StatusProto::FAILED_PRECONDITION);
+    batch_put_result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
+    return batch_put_result_proto;
+  }
+
+  int64_t current_time_ms = clock_->GetSystemTimeMilliseconds();
+
   for (DocumentProto& document_proto :
        *(put_document_request.mutable_documents())) {
     batch_put_result_proto.mutable_put_result_protos()->Add(
-        Put(std::move(document_proto)));
+        PutLocked(std::move(document_proto),
+                  current_time_ms));  // Call the locked version
   }
 
   if (put_document_request.persist_type() != PersistType::UNKNOWN) {
-    *batch_put_result_proto.mutable_persist_to_disk_result_proto() =
-        PersistToDisk(put_document_request.persist_type());
+    // Measure the latency of the persist process.
+    std::unique_ptr<Timer> persist_timer = clock_->GetNewTimer();
+    PersistToDiskStatsProto* persist_stats =
+        batch_put_result_proto.mutable_persist_to_disk_result_proto()
+            ->mutable_persist_stats();
+    auto status =
+        PersistToDiskLocked(put_document_request.persist_type(), persist_stats);
+    TransformStatus(
+        status, batch_put_result_proto.mutable_persist_to_disk_result_proto()
+                    ->mutable_status());
+    persist_stats->set_latency_ms(persist_timer->GetElapsedMilliseconds());
   }
 
+  batch_put_result_proto.mutable_status()->set_code(StatusProto::OK);
+  batch_put_result_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
   return batch_put_result_proto;
 }
 
@@ -1435,8 +1809,31 @@
 }
 
 PutResultProto IcingSearchEngine::Put(DocumentProto&& document) {
+  absl_ports::unique_lock l(&mutex_);
   ICING_VLOG(1) << "Writing document to document store";
 
+  if (!initialized_) {
+    PutResultProto result_proto;
+    result_proto.set_uri(document.uri());
+    StatusProto* result_status = result_proto.mutable_status();
+    result_status->set_code(StatusProto::FAILED_PRECONDITION);
+    result_status->set_message("IcingSearchEngine has not been initialized!");
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
+    return result_proto;
+  }
+
+  int64_t current_time_ms = clock_->GetSystemTimeMilliseconds();
+
+  PutResultProto result_proto = PutLocked(std::move(document), current_time_ms);
+  result_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
+  return result_proto;
+}
+
+// PutLocked to be called when mutex_ is already held.
+PutResultProto IcingSearchEngine::PutLocked(DocumentProto&& document,
+                                            int64_t current_time_ms) {
   PutResultProto result_proto;
   result_proto.set_uri(document.uri());
 
@@ -1450,37 +1847,41 @@
   // Lock must be acquired before validation because the DocumentStore uses
   // the schema file to validate, and the schema could be changed in
   // SetSchema() which is protected by the same mutex.
-  absl_ports::unique_lock l(&mutex_);
-  if (!initialized_) {
-    result_status->set_code(StatusProto::FAILED_PRECONDITION);
-    result_status->set_message("IcingSearchEngine has not been initialized!");
-    return result_proto;
-  }
+  // NO LOCK ACQUISITION HERE - mutex_ is already held.
 
-  auto tokenized_document_or = PrepareDocumentForIndexing(
-      schema_store_.get(), language_segmenter_.get(), std::move(document));
+  auto tokenized_document_or =
+      PrepareDocumentsForIndexing(std::move(document), current_time_ms);
   if (!tokenized_document_or.ok()) {
     TransformStatus(tokenized_document_or.status(), result_status);
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return result_proto;
   }
   TokenizedDocument tokenized_document(
       std::move(tokenized_document_or).ValueOrDie());
 
   auto put_result_or = document_store_->Put(
-      tokenized_document.document(), tokenized_document.num_string_tokens(),
-      put_document_stats);
+      tokenized_document.document_wrapper(), put_document_stats);
   if (!put_result_or.ok()) {
+    // TODO(b/384947619): revisit here if it is document replacement. Determine
+    //   which stage of DocumentStore::Put failed and decide whether to delete
+    //   the document and run delete propagation.
     TransformStatus(put_result_or.status(), result_status);
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return result_proto;
   }
   DocumentId old_document_id = put_result_or.ValueOrDie().old_document_id;
   DocumentId document_id = put_result_or.ValueOrDie().new_document_id;
-  result_proto.set_was_replacement(
-      put_result_or.ValueOrDie().was_replacement());
+  int64_t expiration_timestamp_ms =
+      put_result_or.ValueOrDie().expiration_timestamp_ms;
+  result_proto.set_was_replacement(put_result_or.ValueOrDie().was_replacement);
 
   auto data_indexing_handlers_or = CreateDataIndexingHandlers();
   if (!data_indexing_handlers_or.ok()) {
     TransformStatus(data_indexing_handlers_or.status(), result_status);
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return result_proto;
   }
   IndexProcessor index_processor(
@@ -1493,9 +1894,35 @@
   //   threshold)
   auto index_status = index_processor.IndexDocument(
       tokenized_document, document_id, old_document_id, put_document_stats);
-  // Getting an internal error from the index could possibly mean that the index
-  // is broken. Try to rebuild them to recover.
-  if (absl_ports::IsInternal(index_status)) {
+  if (index_status.ok()) {
+    result_proto.set_document_expiration_timestamp_ms(expiration_timestamp_ms);
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
+    if (options_.enable_delete_propagation_from() &&
+        task_scheduler_ != nullptr) {
+      // Reschedule purging expired document task if:
+      // - expiration_timestamp_ms is not INT64_MAX (i.e. the new document never
+      //   expires). Note: it is fine to schedule a task with at t = INT64_MAX
+      //   since it will never be executed, but let's avoid this edge case.
+      // - AND one of the following is true:
+      //   - existing_scheduled_time_ms < 0: there was no task scheduled, so we
+      //     need to schedule one.
+      //   - expiration_timestamp_ms < existing_scheduled_time_ms: the new
+      //     document has an expiration time that is earlier than the existing
+      //     scheduled.
+      int64_t existing_scheduled_time_ms =
+          task_scheduler_->GetScheduledTimeMs(kHandleExpiredDocumentsTaskId);
+      if (expiration_timestamp_ms < std::numeric_limits<int64_t>::max() &&
+          (existing_scheduled_time_ms < 0 ||
+           expiration_timestamp_ms < existing_scheduled_time_ms)) {
+        task_scheduler_->ScheduleAt(kHandleExpiredDocumentsTaskId,
+                                    CreateHandleExpiredDocumentsTask(),
+                                    expiration_timestamp_ms);
+      }
+    }
+  } else if (absl_ports::IsInternal(index_status)) {
+    // Getting an internal error from the index could possibly mean that the
+    // index is broken. Try to rebuild them to recover.
     ICING_LOG(ERROR) << "Got an internal error from the index. Trying to "
                         "rebuild the index!\n"
                      << index_status.error_message();
@@ -1515,29 +1942,41 @@
   if (!index_status.ok()) {
     // If we encountered a failure or cannot resolve an internal error while
     // indexing this document, then mark it as deleted.
-    int64_t current_time_ms = clock_->GetSystemTimeMilliseconds();
     libtextclassifier3::Status delete_status =
         document_store_->Delete(document_id, current_time_ms);
     if (!delete_status.ok()) {
       // This is pretty dire (and, hopefully, unlikely). We can't roll back the
-      // document that we just added. Wipeout the whole index.
+      // document that we just added. Wipeout the whole database.
       ICING_LOG(ERROR) << "Cannot delete the document that is failed to index. "
                           "Wiping out the whole Icing search engine.";
-      ResetInternal();
+      ResetLocked();
     }
+    // TODO(b/384947619): revisit here and decide whether to propagate delete to
+    //   all dependents if it is a replacement. We might not be able to rely on
+    //   normal delete propagation to remove the dependents here, since the join
+    //   index may be broken at this point.
   }
 
   TransformStatus(index_status, result_status);
+  result_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
   return result_proto;
 }
 
 GetResultProto IcingSearchEngine::Get(const std::string_view name_space,
                                       const std::string_view uri,
                                       const GetResultSpecProto& result_spec) {
+  absl_ports::shared_lock l(&mutex_);
+  return GetLocked(name_space, uri, result_spec);
+}
+
+// GetLocked to be called when mutex_ is already held.
+GetResultProto IcingSearchEngine::GetLocked(
+    const std::string_view name_space, const std::string_view uri,
+    const GetResultSpecProto& result_spec) {
   GetResultProto result_proto;
   StatusProto* result_status = result_proto.mutable_status();
 
-  absl_ports::shared_lock l(&mutex_);
   if (!initialized_) {
     result_status->set_code(StatusProto::FAILED_PRECONDITION);
     result_status->set_message("IcingSearchEngine has not been initialized!");
@@ -1577,6 +2016,105 @@
   return result_proto;
 }
 
+BatchGetResultProto IcingSearchEngine::BatchGet(
+    GetResultSpecProto&& get_result_spec) {
+  const std::string& name_space = get_result_spec.namespace_requested();
+  BatchGetResultProto batch_get_result_proto;
+
+  if (get_result_spec.num_total_document_bytes_to_return() <= 0) {
+    StatusProto* result_status = batch_get_result_proto.mutable_status();
+    result_status->set_code(StatusProto::INVALID_ARGUMENT);
+    result_status->set_message(
+        "num_total_document_bytes_to_return must be greater than 0.");
+    return batch_get_result_proto;
+  }
+
+  absl_ports::shared_lock l(&mutex_);  // Acquire lock once for the batch
+
+  if (!initialized_) {
+    // Handle not initialized case for all documents
+    for (const std::string& id : get_result_spec.ids()) {
+      GetResultProto* result_proto =
+          batch_get_result_proto.mutable_get_result_protos()->Add();
+      result_proto->set_uri(id);
+      result_proto->mutable_status()->set_code(
+          StatusProto::FAILED_PRECONDITION);
+    }
+    batch_get_result_proto.mutable_status()->set_message(
+        "IcingSearchEngine has not been initialized!");
+    batch_get_result_proto.mutable_status()->set_code(
+        StatusProto::FAILED_PRECONDITION);
+    return batch_get_result_proto;
+  }
+
+  int32_t total_docs_bytes_so_far = 0;
+  bool skip_remaining_docs = false;
+  int32_t num_doc_returned = 0;
+  for (std::string& id : *get_result_spec.mutable_ids()) {
+    if (skip_remaining_docs) {
+      // We simply set the status to OUT_OF_SPACE for the remaining docs, even
+      // if some docs might be small enough to fit in the result. We are doing
+      // this to avoid the worst case that all remaining docs are too big, and
+      // we read all of them out but fail to put them in the result.
+      GetResultProto result_proto;
+      // Id is redundant for a single Get call, so we only set it in BatchGet.
+      result_proto.set_uri(std::move(id));
+      StatusProto* result_status = result_proto.mutable_status();
+      result_status->set_code(StatusProto::ABORTED);
+      batch_get_result_proto.mutable_get_result_protos()->Add(
+          std::move(result_proto));
+      continue;
+    }
+
+    // We try to check each doc so smaller docs at the end of the list can
+    // still be put into the result.
+    GetResultProto result_proto =
+        GetLocked(name_space, id, get_result_spec);  // Call locked version
+    // Id is redundant for a single Get call, so we only set it in BatchGet.
+    result_proto.set_uri(std::move(id));
+    if (result_proto.status().code() == StatusProto::OK) {
+      // We get the doc successfully. Check if we can add it to the result.
+      size_t document_bytes = result_proto.document().ByteSizeLong();
+      if (num_doc_returned == 0 ||
+          document_bytes <=
+              get_result_spec.num_total_document_bytes_to_return() -
+                  total_docs_bytes_so_far) {
+        total_docs_bytes_so_far += document_bytes;
+        ++num_doc_returned;
+        // We skip the remaining docs if this 1st doc is already too big.
+        // But we always return 1st one no matter how big it is.
+        if (document_bytes >
+            get_result_spec.num_total_document_bytes_to_return()) {
+          skip_remaining_docs = true;
+        }
+      } else {
+        ICING_LOG(INFO) << "Skipping document due to byte size threshold. "
+                           "Current num docs: "
+                        << num_doc_returned
+                        << ", total byte size: " << total_docs_bytes_so_far
+                        << ", next doc byte size: " << document_bytes
+                        << ", threshold: "
+                        << get_result_spec.num_total_document_bytes_to_return();
+
+        result_proto.clear_document();
+        StatusProto* result_status = result_proto.mutable_status();
+        result_status->set_code(StatusProto::ABORTED);
+        // We skip the remaining docs even if there might be smaller docs
+        // afterwards. This is to avoid the worst case that all remaining docs
+        // are too big, and we read all of them out but fail to put them in the
+        // result.
+        skip_remaining_docs = true;
+      }
+    }
+    batch_get_result_proto.mutable_get_result_protos()->Add(
+        std::move(result_proto));
+  }
+  batch_get_result_proto.mutable_status()->set_code(
+      icing::lib::StatusProto::OK);
+
+  return batch_get_result_proto;
+}
+
 ReportUsageResultProto IcingSearchEngine::ReportUsage(
     const UsageReport& usage_report) {
   ReportUsageResultProto result_proto;
@@ -1586,12 +2124,16 @@
   if (!initialized_) {
     result_status->set_code(StatusProto::FAILED_PRECONDITION);
     result_status->set_message("IcingSearchEngine has not been initialized!");
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return result_proto;
   }
 
   libtextclassifier3::Status status =
       document_store_->ReportUsage(usage_report);
   TransformStatus(status, result_status);
+  result_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
   return result_proto;
 }
 
@@ -1656,11 +2198,14 @@
     // It is possible that the document has expired and the delete operation
     // fails with NOT_FOUND_ERROR. In this case, we should still propagate the
     // delete operation, regardless of the outcome of the delete operation.
-    libtextclassifier3::StatusOr<int> propagated_child_docs_deleted_or =
-        PropagateDelete(/*deleted_document_ids=*/{document_id},
-                        current_time_ms);
+    // TODO(b/384947619): add metadata of propagated documents to
+    //   DeleteResultProto for observer.
+    libtextclassifier3::StatusOr<std::vector<DocumentStore::DocumentMetadata>>
+        propagated_child_docs_deleted_or = PropagateDelete(
+            /*deleted_document_ids=*/{document_id}, current_time_ms);
     if (propagated_child_docs_deleted_or.ok()) {
-      num_documents_deleted += propagated_child_docs_deleted_or.ValueOrDie();
+      num_documents_deleted += static_cast<int>(
+          propagated_child_docs_deleted_or.ValueOrDie().size());
     } else {
       propagate_delete_status =
           std::move(propagated_child_docs_deleted_or).status();
@@ -1874,15 +2419,19 @@
   }
 
   // Propagate deletion.
-  libtextclassifier3::StatusOr<int> propagated_child_docs_deleted_or =
-      PropagateDelete(deleted_document_ids, current_time_ms);
+  // TODO(b/384947619): add metadata of propagated documents to
+  //   DeleteResultProto for observer.
+  libtextclassifier3::StatusOr<std::vector<DocumentStore::DocumentMetadata>>
+      propagated_child_docs_deleted_or =
+          PropagateDelete(deleted_document_ids, current_time_ms);
   if (!propagated_child_docs_deleted_or.ok()) {
     TransformStatus(propagated_child_docs_deleted_or.status(), result_status);
     delete_stats->set_document_removal_latency_ms(
         component_timer->GetElapsedMilliseconds());
     return result_proto;
   }
-  num_deleted += propagated_child_docs_deleted_or.ValueOrDie();
+  num_deleted +=
+      static_cast<int>(propagated_child_docs_deleted_or.ValueOrDie().size());
 
   delete_stats->set_document_removal_latency_ms(
       component_timer->GetElapsedMilliseconds());
@@ -1903,68 +2452,63 @@
   return result_proto;
 }
 
-libtextclassifier3::StatusOr<int> IcingSearchEngine::PropagateDelete(
+// TODO(b/384947619): remove this function once we fully ramp
+// enable_delete_propagation_from.
+libtextclassifier3::StatusOr<std::vector<DocumentStore::DocumentMetadata>>
+IcingSearchEngine::PropagateDelete(
     const std::unordered_set<DocumentId>& deleted_document_ids,
     int64_t current_time_ms) {
-  int propagated_child_docs_deleted = 0;
-
-  if (!options_.enable_delete_propagation_from()) {
-    // No-op if delete propagation is disabled.
-    return propagated_child_docs_deleted;
+  if (!options_.enable_delete_propagation_from() ||
+      deleted_document_ids.empty()) {
+    // No-op if delete propagation is disabled or no deleted document ids, so
+    // return an empty vector.
+    return std::vector<DocumentStore::DocumentMetadata>();
   }
 
-  if (qualified_id_join_index_->version() !=
-      QualifiedIdJoinIndex::Version::kV3) {
-    // This should not happen since Icing should've failed initialization with
-    // delete propagation enabled and join index v3 disabled.
-    // But let's check it here again just in case.
-    return absl_ports::FailedPreconditionError(
-        "Delete propagation is enabled but qualified id join index v3 is not "
-        "used.");
-  }
-
-  // Create join processor to get propagated child documents to delete.
-  JoinProcessor join_processor(document_store_.get(), schema_store_.get(),
-                               qualified_id_join_index_.get(), current_time_ms);
   ICING_ASSIGN_OR_RETURN(
-      std::unordered_set<DocumentId> child_docs_to_delete,
-      join_processor.GetPropagatedChildDocumentsToDelete(deleted_document_ids));
-
-  // Delete all propagated child documents.
-  for (DocumentId child_doc_id : child_docs_to_delete) {
-    auto status = document_store_->Delete(child_doc_id, current_time_ms);
-    if (!status.ok()) {
-      if (absl_ports::IsNotFound(status)) {
-        // The child document has already been deleted or expired, so skip the
-        // error.
-        continue;
-      }
-
-      // Real error.
-      return status;
-    }
-    ++propagated_child_docs_deleted;
-  }
-
-  return propagated_child_docs_deleted;
+      DeletePropagationHandler delete_propagation_handler,
+      DeletePropagationHandler::Create(schema_store_.get(),
+                                       qualified_id_join_index_.get(),
+                                       document_store_.get(), current_time_ms));
+  return delete_propagation_handler.Handle(deleted_document_ids);
 }
 
 PersistToDiskResultProto IcingSearchEngine::PersistToDisk(
     PersistType::Code persist_type) {
-  ICING_VLOG(1) << "Persisting data to disk";
+  // Measure the latency of the persist process.
+  std::unique_ptr<Timer> persist_timer = clock_->GetNewTimer();
 
   PersistToDiskResultProto result_proto;
   StatusProto* result_status = result_proto.mutable_status();
+  PersistToDiskStatsProto* persist_stats = result_proto.mutable_persist_stats();
 
   absl_ports::unique_lock l(&mutex_);
+
+  ICING_LOG(INFO) << "Persisting data to disk with mode "
+                  << static_cast<int>(persist_type);
+
   if (!initialized_) {
+    ICING_LOG(WARNING) << "Attempt to persist data to disk for an "
+                          "uninitialized IcingSearchEngine.";
     result_status->set_code(StatusProto::FAILED_PRECONDITION);
     result_status->set_message("IcingSearchEngine has not been initialized!");
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return result_proto;
   }
 
-  auto status = InternalPersistToDisk(persist_type);
+  auto status = PersistToDiskLocked(persist_type, persist_stats);
+  if (status.ok()) {
+    ICING_LOG(INFO) << "PersistToDisk completed.";
+  } else {
+    ICING_LOG(ERROR) << "PersistToDisk failed. Error code: "
+                     << status.error_code()
+                     << ", message: " << status.error_message();
+  }
   TransformStatus(status, result_status);
+  persist_stats->set_latency_ms(persist_timer->GetElapsedMilliseconds());
+  result_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
   return result_proto;
 }
 
@@ -1975,7 +2519,7 @@
 // 2. Copy data needed to a tmp directory.
 // 3. Swap current directory and tmp directory.
 OptimizeResultProto IcingSearchEngine::Optimize() {
-  ICING_VLOG(1) << "Optimizing icing storage";
+  ICING_LOG(INFO) << "Optimizing icing storage";
 
   OptimizeResultProto result_proto;
   StatusProto* result_status = result_proto.mutable_status();
@@ -1984,6 +2528,8 @@
   if (!initialized_) {
     result_status->set_code(StatusProto::FAILED_PRECONDITION);
     result_status->set_message("IcingSearchEngine has not been initialized!");
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return result_proto;
   }
 
@@ -1992,6 +2538,8 @@
   ScopedTimer optimize_timer(
       clock_->GetNewTimer(),
       [optimize_stats](int64_t t) { optimize_stats->set_latency_ms(t); });
+  result_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
 
   // Read the optimize status and assign previous_optimize_status. This is the
   // time that we last ran optimize.
@@ -2040,11 +2588,26 @@
     }
   }
 
-  // Flushes data to disk before doing optimization
-  auto status = InternalPersistToDisk(PersistType::FULL);
-  if (!status.ok()) {
-    TransformStatus(status, result_status);
-    return result_proto;
+  libtextclassifier3::Status status;
+  std::unique_ptr<Timer> persist_timer;
+  if (!feature_flags_.enable_optimize_improvements()) {
+    // Flushes data to disk before doing optimization.
+    // This really is not necessary. Therefore, if the improvements flag is
+    // enabled, just skip this step.
+    PersistToDiskStatsProto* before_optimize_persist_stats =
+        optimize_stats->mutable_before_optimize_persist_stats();
+
+    persist_timer = clock_->GetNewTimer();
+    status =
+        PersistToDiskLocked(PersistType::FULL, before_optimize_persist_stats);
+    before_optimize_persist_stats->set_latency_ms(
+        persist_timer->GetElapsedMilliseconds());
+    if (!status.ok()) {
+      TransformStatus(status, result_status);
+      result_proto.set_vm_binder_transaction_latency_start_time_ms(
+          clock_->GetSystemTimeMilliseconds());
+      return result_proto;
+    }
   }
 
   // Get all expired blob handles. This can be done before the marker file since
@@ -2072,6 +2635,8 @@
             IcingSearchEngineMarkerProto::OperationType::OPTIMIZE);
     if (!marker_file_or.ok()) {
       TransformStatus(marker_file_or.status(), result_status);
+      result_proto.set_vm_binder_transaction_latency_start_time_ms(
+          clock_->GetSystemTimeMilliseconds());
       return result_proto;
     }
     marker_file = std::move(marker_file_or).ValueOrDie();
@@ -2097,6 +2662,8 @@
     // If INTERNAL_ERROR, we're having IO errors or other errors that we can't
     // recover from.
     TransformStatus(optimize_result_or.status(), result_status);
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return result_proto;
   }
 
@@ -2106,7 +2673,7 @@
     // optimize blob store
     libtextclassifier3::StatusOr<std::vector<std::string>>
         blob_file_names_to_remove_or = blob_store_->Optimize(
-            optimize_result_or.ValueOrDie().dead_blob_handles);
+            optimize_result_or.ValueOrDie().dead_blob_handles, &feature_flags_);
     if (!blob_file_names_to_remove_or.ok()) {
       TransformStatus(blob_file_names_to_remove_or.status(), result_status);
       return result_proto;
@@ -2157,7 +2724,7 @@
 
     libtextclassifier3::Status qualified_id_join_index_optimize_status =
         qualified_id_join_index_->Optimize(
-            optimize_result.document_id_old_to_new,
+            document_store_.get(), optimize_result.document_id_old_to_new,
             optimize_result.namespace_id_old_to_new,
             document_store_->last_added_document_id());
     if (!qualified_id_join_index_optimize_status.ok()) {
@@ -2198,6 +2765,8 @@
       TransformStatus(status, result_status);
       optimize_stats->set_index_restoration_latency_ms(
           optimize_index_timer->GetElapsedMilliseconds());
+      result_proto.set_vm_binder_transaction_latency_start_time_ms(
+          clock_->GetSystemTimeMilliseconds());
       return result_proto;
     }
 
@@ -2215,6 +2784,8 @@
       TransformStatus(status, result_status);
       optimize_stats->set_index_restoration_latency_ms(
           optimize_index_timer->GetElapsedMilliseconds());
+      result_proto.set_vm_binder_transaction_latency_start_time_ms(
+          clock_->GetSystemTimeMilliseconds());
       return result_proto;
     }
   }
@@ -2246,9 +2817,16 @@
   }
 
   // Flushes data to disk after doing optimization
-  status = InternalPersistToDisk(PersistType::FULL);
+  PersistToDiskStatsProto* after_optimize_persist_stats =
+      optimize_stats->mutable_after_optimize_persist_stats();
+  persist_timer = clock_->GetNewTimer();
+  status = PersistToDiskLocked(PersistType::FULL, after_optimize_persist_stats);
+  after_optimize_persist_stats->set_latency_ms(
+      persist_timer->GetElapsedMilliseconds());
   if (!status.ok()) {
     TransformStatus(status, result_status);
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return result_proto;
   }
 
@@ -2257,6 +2835,9 @@
       Filesystem::SanitizeFileSize(after_size));
 
   TransformStatus(doc_store_optimize_result_status, result_status);
+  result_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
+  ICING_LOG(INFO) << "Finished optimizing icing storage";
   return result_proto;
 }
 
@@ -2270,36 +2851,52 @@
   if (!initialized_) {
     result_status->set_code(StatusProto::FAILED_PRECONDITION);
     result_status->set_message("IcingSearchEngine has not been initialized!");
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return result_proto;
   }
 
+  int64_t current_time_ms = clock_->GetSystemTimeMilliseconds();
+
   // Read the optimize status to get the time that we last ran.
   std::string optimize_status_filename =
       absl_ports::StrCat(options_.base_dir(), "/", kOptimizeStatusFilename);
   FileBackedProto<OptimizeStatusProto> optimize_status_file(
       *filesystem_, optimize_status_filename);
   auto optimize_status_or = optimize_status_file.Read();
-  int64_t current_time = clock_->GetSystemTimeMilliseconds();
 
-  if (optimize_status_or.ok()) {
-    // If we have trouble reading the status or this is the first time that
-    // we've ever run, don't set this field.
+  if (!optimize_status_or.ok()) {
+    // We have trouble reading the status; or we've never run optimize before.
+    result_proto.set_no_previous_optimize_info(true);
+  } else {
     int64_t time_since_last_optimize_ms;
     if (options_.calculate_time_since_last_attempted_optimize()) {
       time_since_last_optimize_ms = GetTimeSinceLastOptimizeMs(
-          current_time, *optimize_status_or.ValueOrDie());
+          current_time_ms, *optimize_status_or.ValueOrDie());
     } else {
       time_since_last_optimize_ms =
-          current_time - optimize_status_or.ValueOrDie()
-                             ->last_successful_optimize_run_time_ms();
+          current_time_ms - optimize_status_or.ValueOrDie()
+                                ->last_successful_optimize_run_time_ms();
+    }
+    if (time_since_last_optimize_ms < 0) {
+      ICING_LOG(WARNING) << "Time since last optimize is negative: "
+                         << time_since_last_optimize_ms
+                         << ". Setting no_previous_optimize_info to true.";
+      result_proto.set_no_previous_optimize_info(true);
     }
     result_proto.set_time_since_last_optimize_ms(time_since_last_optimize_ms);
   }
 
+  // Get stats from ResultStateManager.
+  result_proto.set_num_active_result_states(
+      result_state_manager_->GetNumActiveResultStates(current_time_ms));
+
   // Get stats from DocumentStore
   auto doc_store_optimize_info_or = document_store_->GetOptimizeInfo();
   if (!doc_store_optimize_info_or.ok()) {
     TransformStatus(doc_store_optimize_info_or.status(), result_status);
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return result_proto;
   }
   DocumentStore::OptimizeInfo doc_store_optimize_info =
@@ -2310,6 +2907,8 @@
     // Can return early since there's nothing to calculate on the index side
     result_proto.set_estimated_optimizable_bytes(0);
     result_status->set_code(StatusProto::OK);
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return result_proto;
   }
 
@@ -2317,6 +2916,8 @@
   auto index_elements_size_or = index_->GetElementsSize();
   if (!index_elements_size_or.ok()) {
     TransformStatus(index_elements_size_or.status(), result_status);
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return result_proto;
   }
   int64_t index_elements_size = index_elements_size_or.ValueOrDie();
@@ -2330,6 +2931,8 @@
       doc_store_optimize_info.estimated_optimizable_bytes);
 
   result_status->set_code(StatusProto::OK);
+  result_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
   return result_proto;
 }
 
@@ -2358,6 +2961,8 @@
       result.mutable_status()->set_code(StatusProto::INTERNAL);
       result.mutable_status()->set_message(
           namespace_blob_storage_infos_or.status().error_message());
+      result.set_vm_binder_transaction_latency_start_time_ms(
+          clock_->GetSystemTimeMilliseconds());
       return result;
     }
     std::vector<NamespaceBlobStorageInfoProto> namespace_blob_storage_infos =
@@ -2372,6 +2977,8 @@
   }
   // TODO(b/259744228): add stats for integer index
   result.mutable_status()->set_code(StatusProto::OK);
+  result.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
   return result;
 }
 
@@ -2417,30 +3024,79 @@
   return debug_info;
 }
 
-libtextclassifier3::Status IcingSearchEngine::InternalPersistToDisk(
-    PersistType::Code persist_type) {
+libtextclassifier3::Status IcingSearchEngine::PersistToDiskLocked(
+    PersistType::Code persist_type, PersistToDiskStatsProto* persist_stats) {
+  std::unique_ptr<Timer> overall_timer = clock_->GetNewTimer();
+  persist_stats->set_persist_type(persist_type);
+
   if (blob_store_ != nullptr) {
     // For all valid PersistTypes, we persist the ground truth. The ground truth
     // in the blob_store is a proto log file, which is need to be called when
     // persist_type is LITE.
     ICING_RETURN_IF_ERROR(blob_store_->PersistToDisk());
+    persist_stats->set_blob_store_persist_latency_ms(
+        overall_timer->GetElapsedMilliseconds());
   }
-  ICING_RETURN_IF_ERROR(document_store_->PersistToDisk(persist_type));
+
+  overall_timer = clock_->GetNewTimer();
+  ICING_RETURN_IF_ERROR(
+      document_store_->PersistToDisk(persist_type, persist_stats));
+  persist_stats->set_document_store_total_persist_latency_ms(
+      overall_timer->GetElapsedMilliseconds());
+
   if (persist_type == PersistType::RECOVERY_PROOF) {
     // Persist RECOVERY_PROOF will persist the ground truth and then update all
     // checksums. There is no need to call document_store_->UpdateChecksum()
     // because PersistToDisk(RECOVERY_PROOF) will update the checksum anyways.
+    overall_timer = clock_->GetNewTimer();
     ICING_RETURN_IF_ERROR(schema_store_->UpdateChecksum());
+    persist_stats->set_schema_store_persist_latency_ms(
+        overall_timer->GetElapsedMilliseconds());
+
+    overall_timer = clock_->GetNewTimer();
     index_->UpdateChecksum();
+    persist_stats->set_index_persist_latency_ms(
+        overall_timer->GetElapsedMilliseconds());
+
+    overall_timer = clock_->GetNewTimer();
     ICING_RETURN_IF_ERROR(integer_index_->UpdateChecksums());
+    persist_stats->set_integer_index_persist_latency_ms(
+        overall_timer->GetElapsedMilliseconds());
+
+    overall_timer = clock_->GetNewTimer();
     ICING_RETURN_IF_ERROR(qualified_id_join_index_->UpdateChecksums());
+    persist_stats->set_qualified_id_join_index_persist_latency_ms(
+        overall_timer->GetElapsedMilliseconds());
+
+    overall_timer = clock_->GetNewTimer();
     ICING_RETURN_IF_ERROR(embedding_index_->UpdateChecksums());
+    persist_stats->set_embedding_index_persist_latency_ms(
+        overall_timer->GetElapsedMilliseconds());
   } else if (persist_type == PersistType::FULL) {
+    overall_timer = clock_->GetNewTimer();
     ICING_RETURN_IF_ERROR(schema_store_->PersistToDisk());
+    persist_stats->set_schema_store_persist_latency_ms(
+        overall_timer->GetElapsedMilliseconds());
+
+    overall_timer = clock_->GetNewTimer();
     ICING_RETURN_IF_ERROR(index_->PersistToDisk());
+    persist_stats->set_index_persist_latency_ms(
+        overall_timer->GetElapsedMilliseconds());
+
+    overall_timer = clock_->GetNewTimer();
     ICING_RETURN_IF_ERROR(integer_index_->PersistToDisk());
+    persist_stats->set_integer_index_persist_latency_ms(
+        overall_timer->GetElapsedMilliseconds());
+
+    overall_timer = clock_->GetNewTimer();
     ICING_RETURN_IF_ERROR(qualified_id_join_index_->PersistToDisk());
+    persist_stats->set_qualified_id_join_index_persist_latency_ms(
+        overall_timer->GetElapsedMilliseconds());
+
+    overall_timer = clock_->GetNewTimer();
     ICING_RETURN_IF_ERROR(embedding_index_->PersistToDisk());
+    persist_stats->set_embedding_index_persist_latency_ms(
+        overall_timer->GetElapsedMilliseconds());
   }
 
   return libtextclassifier3::Status::OK;
@@ -2468,12 +3124,14 @@
   int64_t lock_acquisition_latency = overall_timer->GetElapsedMilliseconds();
 
   SearchResultProto result_proto =
-      InternalSearch(search_spec, scoring_spec, result_spec);
+      SearchLocked(search_spec, scoring_spec, result_spec);
 
   result_proto.mutable_query_stats()->set_lock_acquisition_latency_ms(
       lock_acquisition_latency);
   result_proto.mutable_query_stats()->set_latency_ms(
       overall_timer->GetElapsedMilliseconds());
+  result_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
   return result_proto;
 }
 
@@ -2487,16 +3145,18 @@
   int64_t lock_acquisition_latency = overall_timer->GetElapsedMilliseconds();
 
   SearchResultProto result_proto =
-      InternalSearch(search_spec, scoring_spec, result_spec);
+      SearchLocked(search_spec, scoring_spec, result_spec);
 
   result_proto.mutable_query_stats()->set_lock_acquisition_latency_ms(
       lock_acquisition_latency);
   result_proto.mutable_query_stats()->set_latency_ms(
       overall_timer->GetElapsedMilliseconds());
+  result_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
   return result_proto;
 }
 
-SearchResultProto IcingSearchEngine::InternalSearch(
+SearchResultProto IcingSearchEngine::SearchLocked(
     const SearchSpecProto& search_spec, const ScoringSpecProto& scoring_spec,
     const ResultSpecProto& result_spec) {
   SearchResultProto result_proto;
@@ -2696,9 +3356,9 @@
   std::unique_ptr<Timer> component_timer = clock_->GetNewTimer();
   // CacheAndRetrieveFirstPage and retrieves the document protos and snippets if
   // requested
-  auto result_retriever_or =
-      ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get());
+  auto result_retriever_or = ResultRetrieverV2::Create(
+      document_store_.get(), schema_store_.get(), language_segmenter_.get(),
+      normalizer_.get(), &feature_flags_);
   if (!result_retriever_or.ok()) {
     TransformStatus(result_retriever_or.status(), result_status);
     query_stats->set_document_retrieval_latency_ms(
@@ -2712,7 +3372,7 @@
       page_result_info_or = result_state_manager_->CacheAndRetrieveFirstPage(
           std::move(ranker), std::move(parent_result_adjustment_info),
           std::move(child_result_adjustment_info), result_spec,
-          *document_store_, *result_retriever, current_time_ms);
+          *document_store_, *result_retriever, current_time_ms, query_stats);
   if (!page_result_info_or.ok()) {
     TransformStatus(page_result_info_or.status(), result_status);
     query_stats->set_document_retrieval_latency_ms(
@@ -2750,6 +3410,32 @@
   return result_proto;
 }
 
+libtextclassifier3::StatusOr<TokenizedDocument>
+IcingSearchEngine::PrepareDocumentsForIndexing(DocumentProto&& document,
+                                               int64_t current_time_ms) {
+  ICING_ASSIGN_OR_RETURN(
+      TokenizedDocument tokenized_document,
+      TokenizedDocument::Create(schema_store_.get(), language_segmenter_.get(),
+                                current_time_ms, std::move(document)));
+
+  if (!options_.enable_delete_propagation_from()) {
+    return tokenized_document;
+  }
+
+  // Make a temporary vector to hold the single tokenized document for
+  // DocumentDependencyProcessor.
+  std::vector<TokenizedDocument> tmp_tokenized_documents;
+  tmp_tokenized_documents.push_back(std::move(tokenized_document));
+
+  ICING_ASSIGN_OR_RETURN(
+      DocumentDependencyProcessor dependency_processor,
+      DocumentDependencyProcessor::Create(
+          document_store_.get(), tmp_tokenized_documents, current_time_ms));
+  ICING_RETURN_IF_ERROR(dependency_processor.Evaluate());
+
+  return std::move(tmp_tokenized_documents[0]);
+}
+
 IcingSearchEngine::QueryScoringResults IcingSearchEngine::ProcessQueryAndScore(
     const SearchSpecProto& search_spec, const ScoringSpecProto& scoring_spec,
     const ResultSpecProto& result_spec,
@@ -2842,6 +3528,14 @@
 }
 
 SearchResultProto IcingSearchEngine::GetNextPage(uint64_t next_page_token) {
+  GetNextPageRequestProto request_proto;
+  request_proto.set_next_page_token(next_page_token);
+
+  return GetNextPage(std::move(request_proto));
+}
+
+SearchResultProto IcingSearchEngine::GetNextPage(
+    GetNextPageRequestProto&& get_next_page_request) {
   SearchResultProto result_proto;
   StatusProto* result_status = result_proto.mutable_status();
 
@@ -2859,9 +3553,9 @@
     return result_proto;
   }
 
-  auto result_retriever_or =
-      ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get());
+  auto result_retriever_or = ResultRetrieverV2::Create(
+      document_store_.get(), schema_store_.get(), language_segmenter_.get(),
+      normalizer_.get(), &feature_flags_);
   if (!result_retriever_or.ok()) {
     TransformStatus(result_retriever_or.status(), result_status);
     return result_proto;
@@ -2869,14 +3563,32 @@
   std::unique_ptr<ResultRetrieverV2> result_retriever =
       std::move(result_retriever_or).ValueOrDie();
 
+  uint64_t next_page_token = get_next_page_request.next_page_token();
   int64_t current_time_ms = clock_->GetSystemTimeMilliseconds();
   libtextclassifier3::StatusOr<std::pair<uint64_t, PageResult>>
       page_result_info_or = result_state_manager_->GetNextPage(
-          next_page_token, *result_retriever, current_time_ms);
+          next_page_token,
+          get_next_page_request.max_results_to_retrieve_from_page(),
+          *result_retriever, current_time_ms);
   if (!page_result_info_or.ok()) {
     if (absl_ports::IsNotFound(page_result_info_or.status())) {
-      // NOT_FOUND means an empty result.
+      // - If calling GetNextPage with an invalid page token, return OK with an
+      //   empty result.
+      // - If the token is valid but getting NOT_FOUND error, then it is likely
+      //   that the corresponding ResultState has been removed due to cache
+      //   eviction (caused by cache budget limit exceeded or
+      //   SetSchema/Optimize). In this case, we should return an additional
+      //   (warning) field to the client indicating that the pagination is
+      //   incomplete.
       result_status->set_code(StatusProto::OK);
+
+      if (next_page_token != kInvalidNextPageToken) {
+        result_proto.set_page_token_not_found(true);
+        query_stats->set_page_token_type(
+            QueryStatsProto::PageTokenType::NOT_FOUND);
+      } else {
+        query_stats->set_page_token_type(QueryStatsProto::PageTokenType::EMPTY);
+      }
     } else {
       // Real error, pass up.
       TransformStatus(page_result_info_or.status(), result_status);
@@ -2916,6 +3628,7 @@
   query_stats->set_num_results_with_snippets(
       page_result_info.second.num_results_with_snippets);
   query_stats->set_num_joined_results_returned_current_page(child_count);
+  query_stats->set_page_token_type(QueryStatsProto::PageTokenType::VALID);
 
   return result_proto;
 }
@@ -2929,6 +3642,119 @@
   result_state_manager_->InvalidateResultState(next_page_token);
 }
 
+HandleExpiredDocumentsResultProto IcingSearchEngine::HandleExpiredDocuments() {
+  absl_ports::unique_lock l(&mutex_);
+
+  if (!initialized_) {
+    HandleExpiredDocumentsResultProto result_proto;
+    StatusProto* result_status = result_proto.mutable_status();
+    result_status->set_code(StatusProto::FAILED_PRECONDITION);
+    result_status->set_message("IcingSearchEngine has not been initialized!");
+    return result_proto;
+  }
+
+  return HandleExpiredDocumentsLocked();
+}
+
+HandleExpiredDocumentsResultProto
+IcingSearchEngine::HandleExpiredDocumentsLocked() {
+  HandleExpiredDocumentsResultProto result_proto;
+  StatusProto* result_status = result_proto.mutable_status();
+
+  // We don't need to purge expired documents before delete propagation is
+  // supported, since in search API it will automatically filter out expired
+  // documents. However, when delete propagation is enabled, we need to purge
+  // expired documents explicitly in order to propagate deletion to their
+  // children. Therefore, this API is implemented and can be released together
+  // with the delete propagation feature.
+  if (!options_.enable_delete_propagation_from()) {
+    result_status->set_code(StatusProto::FAILED_PRECONDITION);
+    result_status->set_message(
+        "Delete propagation is not enabled in this Icing instance!");
+    return result_proto;
+  }
+
+  int64_t current_time_ms = clock_->GetSystemTimeMilliseconds();
+  // Step 1: purge expired documents.
+  auto expired_docs_or =
+      document_store_->PurgeExpiredDocuments(current_time_ms);
+  if (!expired_docs_or.ok()) {
+    TransformStatus(expired_docs_or.status(), result_status);
+    return result_proto;
+  }
+  std::vector<DocumentStore::DocumentMetadata> expired_docs =
+      std::move(expired_docs_or).ValueOrDie();
+
+  // Step 2: propagate deletion to their children.
+  std::unordered_set<DocumentId> expired_doc_ids;
+  expired_doc_ids.reserve(expired_docs.size());
+  for (const DocumentStore::DocumentMetadata& metadata : expired_docs) {
+    expired_doc_ids.insert(metadata.document_id);
+  }
+  auto propagated_deleted_docs_or =
+      PropagateDelete(expired_doc_ids, current_time_ms);
+  if (!propagated_deleted_docs_or.ok()) {
+    TransformStatus(propagated_deleted_docs_or.status(), result_status);
+    return result_proto;
+  }
+  std::vector<DocumentStore::DocumentMetadata> propagated_deleted_docs =
+      std::move(propagated_deleted_docs_or).ValueOrDie();
+
+  // Get the next expiration timestamp.
+  int64_t next_expired_doc_ts_ms =
+      document_store_->GetNextExpiredDocumentTimestampMs(current_time_ms);
+  result_proto.set_next_expiration_timestamp_ms(next_expired_doc_ts_ms);
+
+  // Step 3: reschedule the task to handle next expired document(s).
+  if (task_scheduler_ != nullptr) {
+    if (next_expired_doc_ts_ms < 0) {
+      // No more expired documents. Cancel the scheduled task.
+      task_scheduler_->Cancel(kHandleExpiredDocumentsTaskId);
+    } else {
+      task_scheduler_->ScheduleAt(kHandleExpiredDocumentsTaskId,
+                                  CreateHandleExpiredDocumentsTask(),
+                                  next_expired_doc_ts_ms);
+    }
+  }
+
+  result_proto.set_num_expired_documents(
+      static_cast<int32_t>(expired_docs.size()));
+  result_proto.set_num_propagated_deleted_documents(
+      static_cast<int32_t>(propagated_deleted_docs.size()));
+
+  // Add all deleted documents to the result proto, grouped by NamespaceTypePair
+  // (namespace, schema).
+  std::unordered_map<NamespaceTypePair,
+                     HandleExpiredDocumentsResultProto::DocumentGroupInfo*,
+                     NamespaceTypePairHasher>
+      group_map;
+  const auto add_fn =
+      [&result_proto,
+       &group_map](std::vector<DocumentStore::DocumentMetadata>&& metadata_list)
+      -> void {
+    for (DocumentStore::DocumentMetadata& metadata : metadata_list) {
+      NamespaceTypePair group_key = {std::move(metadata.name_space),
+                                     std::move(metadata.schema_type_name)};
+      auto itr = group_map.find(group_key);
+      if (itr == group_map.end()) {
+        HandleExpiredDocumentsResultProto::DocumentGroupInfo* entry =
+            result_proto.add_deleted_documents();
+        entry->set_name_space(group_key.namespace_);
+        entry->set_schema(group_key.type);
+        entry->add_uris(std::move(metadata.uri));
+        group_map.insert({std::move(group_key), entry});
+      } else {
+        itr->second->add_uris(std::move(metadata.uri));
+      }
+    }
+  };
+  add_fn(std::move(expired_docs));
+  add_fn(std::move(propagated_deleted_docs));
+
+  result_status->set_code(StatusProto::OK);
+  return result_proto;
+}
+
 BlobProto IcingSearchEngine::OpenWriteBlob(
     const PropertyProto::BlobHandleProto& blob_handle) {
   BlobProto blob_proto;
@@ -2945,10 +3771,14 @@
   if (!initialized_) {
     status->set_code(StatusProto::FAILED_PRECONDITION);
     status->set_message("IcingSearchEngine has not been initialized!");
+    blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return blob_proto;
   }
-
-  return blob_store_->OpenWrite(blob_handle);
+  blob_proto = blob_store_->OpenWrite(blob_handle);
+  blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
+  return blob_proto;
 }
 
 BlobProto IcingSearchEngine::RemoveBlob(
@@ -2960,16 +3790,22 @@
   if (blob_store_ == nullptr) {
     status->set_code(StatusProto::FAILED_PRECONDITION);
     status->set_message("Remove blob is not supported in this Icing instance!");
+    blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return blob_proto;
   }
 
   if (!initialized_) {
     status->set_code(StatusProto::FAILED_PRECONDITION);
     status->set_message("IcingSearchEngine has not been initialized!");
+    blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return blob_proto;
   }
-
-  return blob_store_->RemoveBlob(blob_handle);
+  blob_proto = blob_store_->RemoveBlob(blob_handle);
+  blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
+  return blob_proto;
 }
 
 BlobProto IcingSearchEngine::OpenReadBlob(
@@ -2981,17 +3817,23 @@
     status->set_code(StatusProto::FAILED_PRECONDITION);
     status->set_message(
         "Open read blob is not supported in this Icing instance!");
+    blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return blob_proto;
   }
 
   if (!initialized_) {
     status->set_code(StatusProto::FAILED_PRECONDITION);
     status->set_message("IcingSearchEngine has not been initialized!");
+    blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     ICING_LOG(ERROR) << status->message();
     return blob_proto;
   }
-
-  return blob_store_->OpenRead(blob_handle);
+  blob_proto = blob_store_->OpenRead(blob_handle);
+  blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
+  return blob_proto;
 }
 
 BlobProto IcingSearchEngine::CommitBlob(
@@ -3002,6 +3844,8 @@
   if (blob_store_ == nullptr) {
     status->set_code(StatusProto::FAILED_PRECONDITION);
     status->set_message("Commit blob is not supported in this Icing instance!");
+    blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return blob_proto;
   }
 
@@ -3009,10 +3853,69 @@
     status->set_code(StatusProto::FAILED_PRECONDITION);
     status->set_message("IcingSearchEngine has not been initialized!");
     ICING_LOG(ERROR) << status->message();
+    blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
+    return blob_proto;
+  }
+  blob_proto = blob_store_->CommitBlob(blob_handle);
+  blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
+  return blob_proto;
+}
+
+BlobProto IcingSearchEngine::GetAllBlobInfos() {
+  BlobProto blob_proto;
+  StatusProto* status = blob_proto.mutable_status();
+  absl_ports::unique_lock l(&mutex_);
+  if (blob_store_ == nullptr) {
+    status->set_code(StatusProto::FAILED_PRECONDITION);
+    status->set_message(
+        "Get all blob info is not supported in this Icing instance!");
+    blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return blob_proto;
   }
 
-  return blob_store_->CommitBlob(blob_handle);
+  if (!initialized_) {
+    status->set_code(StatusProto::FAILED_PRECONDITION);
+    status->set_message("IcingSearchEngine has not been initialized!");
+    ICING_LOG(ERROR) << status->message();
+    blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
+    return blob_proto;
+  }
+  blob_proto = blob_store_->GetAllBlobInfos();
+  blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
+  return blob_proto;
+}
+
+BlobProto IcingSearchEngine::PutBlobInfos(const BlobProto& blob_info_protos) {
+  BlobProto result_blob_proto;
+  StatusProto* status = result_blob_proto.mutable_status();
+  absl_ports::unique_lock l(&mutex_);
+  if (blob_store_ == nullptr) {
+    status->set_code(StatusProto::FAILED_PRECONDITION);
+    status->set_message(
+        "Put blob info is not supported in this Icing instance!");
+    result_blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
+    return result_blob_proto;
+  }
+
+  if (!initialized_) {
+    status->set_code(StatusProto::FAILED_PRECONDITION);
+    status->set_message("IcingSearchEngine has not been initialized!");
+    ICING_LOG(ERROR) << status->message();
+    result_blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
+    return result_blob_proto;
+  }
+
+  result_blob_proto = blob_store_->PutBlobInfos(blob_info_protos);
+  result_blob_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
+  return result_blob_proto;
 }
 
 libtextclassifier3::StatusOr<DocumentStore::OptimizeResult>
@@ -3049,6 +3952,8 @@
 
   // result_state_manager_ depends on document_store_. So we need to reset it at
   // the same time that we reset the document_store_.
+  ICING_LOG(INFO) << "Resetting result state manager due to optimize. All "
+                     "existing result states will be invalidated.";
   result_state_manager_.reset();
   document_store_.reset();
 
@@ -3076,7 +3981,9 @@
         schema_store_.get(), &feature_flags_,
         /*force_recovery_and_revalidate_documents=*/false,
         /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
-        options_.compression_level(), /*initialize_stats=*/nullptr);
+        options_.compression_level(), options_.compression_threshold_bytes(),
+        options_.compression_mem_level(),
+        /*initialize_stats=*/nullptr);
     // TODO(b/144458732): Implement a more robust version of
     // TC_ASSIGN_OR_RETURN that can support error logging.
     if (!create_result_or.ok()) {
@@ -3104,7 +4011,9 @@
       schema_store_.get(), &feature_flags_,
       /*force_recovery_and_revalidate_documents=*/false,
       /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
-      options_.compression_level(), /*initialize_stats=*/nullptr);
+      options_.compression_level(), options_.compression_threshold_bytes(),
+      options_.compression_mem_level(),
+      /*initialize_stats=*/nullptr);
   if (!create_result_or.ok()) {
     // Unable to create DocumentStore from the new file. Mark as uninitialized
     // and return INTERNAL.
@@ -3213,9 +4122,7 @@
 
     libtextclassifier3::Status status;
     libtextclassifier3::StatusOr<TokenizedDocument> tokenized_document_or =
-        PrepareDocumentForIndexing(schema_store_.get(),
-                                   language_segmenter_.get(),
-                                   std::move(document));
+        PrepareDocumentsForIndexing(std::move(document), current_time_ms);
     if (!tokenized_document_or.ok()) {
       status = std::move(tokenized_document_or).status();
     } else {
@@ -3252,20 +4159,39 @@
     }
   }
 
-  // Finally, delete all failed documents.
+  // Delete all failed documents and propagate deletion to their child
+  // documents. If there is any error, log it without failing index restoration.
+  // TODO(b/384947619): add metadata of deleted documents into
+  //   InitializeResultProto.
   if (options_.enable_soft_index_restoration()) {
     for (DocumentId document_id : failed_document_ids) {
-      libtextclassifier3::Status delete_status =
-          document_store_->Delete(document_id, current_time_ms);
-      if (!delete_status.ok()) {
+      libtextclassifier3::StatusOr<DocumentStore::DocumentMetadata>
+          deleted_metadata_or = document_store_->ForceDelete(document_id);
+      if (!deleted_metadata_or.ok()) {
         // This is pretty dire (and, hopefully, unlikely). Log the error and
         // skip it.
         ICING_LOG(WARNING) << "Cannot delete document " << document_id
                            << " that which failed to index: "
-                           << delete_status.error_message();
+                           << deleted_metadata_or.status().error_message();
       }
     }
-    // TODO(b/384947619): apply delete propagation on these failed documents.
+
+    // Propagate deletion to child documents. Call PropagateDelete directly here
+    // since the delete propagation flag will be checked there.
+    auto propagated_deleted_child_docs_or =
+        PropagateDelete(failed_document_ids, current_time_ms);
+    if (!propagated_deleted_child_docs_or.ok()) {
+      ICING_LOG(WARNING)
+          << "Cannot propagate deletion for child documents of failed "
+             "documents during index restoration: "
+          << propagated_deleted_child_docs_or.status().error_message();
+    } else {
+      ICING_LOG(INFO)
+          << "Successfully deleted " << failed_document_ids.size()
+          << " documents that failed to index, and propagated deletion to "
+          << propagated_deleted_child_docs_or.ValueOrDie().size()
+          << " child documents during index restoration.";
+    }
   }
 
   return IndexRestorationResult(std::move(overall_status),
@@ -3317,11 +4243,11 @@
   handlers.push_back(std::move(integer_section_indexing_handler));
 
   // Qualified id join index handler
-  ICING_ASSIGN_OR_RETURN(
-      std::unique_ptr<QualifiedIdJoinIndexingHandler>
-          qualified_id_join_indexing_handler,
-      QualifiedIdJoinIndexingHandler::Create(
-          clock_.get(), document_store_.get(), qualified_id_join_index_.get()));
+  ICING_ASSIGN_OR_RETURN(std::unique_ptr<QualifiedIdJoinIndexingHandler>
+                             qualified_id_join_indexing_handler,
+                         QualifiedIdJoinIndexingHandler::Create(
+                             clock_.get(), document_store_.get(),
+                             qualified_id_join_index_.get(), &feature_flags_));
   handlers.push_back(std::move(qualified_id_join_indexing_handler));
 
   // Embedding index handler
@@ -3533,25 +4459,62 @@
   return libtextclassifier3::Status::OK;
 }
 
-ResetResultProto IcingSearchEngine::Reset() {
-  absl_ports::unique_lock l(&mutex_);
-  return ResetInternal();
+ResetResultProto IcingSearchEngine::ClearAndDestroy() {
+  // Destroy the task scheduler first to make sure the background tasks finish
+  // before we delete the IcingSearchEngine object.
+  //
+  // Note: it is fine to not destroy the task scheduler since all the tasks will
+  //   fail silently due to initialized_ being false, but let's do it for
+  //   cleanup.
+  DestroyTaskScheduler();
+
+  {
+    absl_ports::unique_lock l(&mutex_);
+    ResetResultProto result_proto = ClearAndDestroyLocked();
+    result_proto.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
+    return result_proto;
+  }
 }
 
-ResetResultProto IcingSearchEngine::ResetInternal() {
-  ICING_VLOG(1) << "Resetting IcingSearchEngine";
+ResetResultProto IcingSearchEngine::ClearAndDestroyLocked() {
+  ICING_LOG(INFO) << "Removing Icing Search Engine directory: "
+                  << options_.base_dir() << ".";
 
   ResetResultProto result_proto;
   StatusProto* result_status = result_proto.mutable_status();
 
   initialized_ = false;
-  ResetMembers();
+  ResetMembersLocked();
   if (!filesystem_->DeleteDirectoryRecursively(options_.base_dir().c_str())) {
     result_status->set_code(StatusProto::INTERNAL);
     return result_proto;
   }
 
-  if (InternalInitialize().status().code() != StatusProto::OK) {
+  result_status->set_code(StatusProto::OK);
+  return result_proto;
+}
+
+ResetResultProto IcingSearchEngine::Reset() {
+  absl_ports::unique_lock l(&mutex_);
+  ResetResultProto result_proto = ResetLocked();
+  result_proto.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
+  return result_proto;
+}
+
+ResetResultProto IcingSearchEngine::ResetLocked() {
+  ICING_VLOG(1) << "Resetting IcingSearchEngine";
+
+  ResetResultProto result_proto;
+  StatusProto* result_status = result_proto.mutable_status();
+
+  if (ClearAndDestroyLocked().status().code() != StatusProto::OK) {
+    result_status->set_code(StatusProto::INTERNAL);
+    return result_proto;
+  }
+
+  if (InitializeLocked().status().code() != StatusProto::OK) {
     // We shouldn't hit the following Initialize errors:
     //   NOT_FOUND: all data was cleared, we aren't expecting anything
     //   DATA_LOSS: all data was cleared, we aren't expecting anything
@@ -3580,6 +4543,8 @@
   if (!initialized_) {
     response_status->set_code(StatusProto::FAILED_PRECONDITION);
     response_status->set_message("IcingSearchEngine has not been initialized!");
+    response.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return response;
   }
 
@@ -3587,6 +4552,8 @@
       ValidateSuggestionSpec(suggestion_spec, performance_configuration_);
   if (!status.ok()) {
     TransformStatus(status, response_status);
+    response.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return response;
   }
 
@@ -3597,6 +4564,8 @@
       schema_store_.get(), clock_.get(), &feature_flags_);
   if (!suggestion_processor_or.ok()) {
     TransformStatus(suggestion_processor_or.status(), response_status);
+    response.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return response;
   }
   std::unique_ptr<SuggestionProcessor> suggestion_processor =
@@ -3608,6 +4577,8 @@
       suggestion_processor->QuerySuggestions(suggestion_spec, current_time_ms);
   if (!terms_or.ok()) {
     TransformStatus(terms_or.status(), response_status);
+    response.set_vm_binder_transaction_latency_start_time_ms(
+        clock_->GetSystemTimeMilliseconds());
     return response;
   }
 
@@ -3618,8 +4589,43 @@
     response.mutable_suggestions()->Add(std::move(suggestion));
   }
   response_status->set_code(StatusProto::OK);
+  response.set_vm_binder_transaction_latency_start_time_ms(
+      clock_->GetSystemTimeMilliseconds());
   return response;
 }
 
+void IcingSearchEngine::DestroyTaskScheduler() {
+  // We have to acquire the lock before accessing and resetting task_scheduler_.
+  // Otherwise, if the background task is still running in the critical section
+  // and accessing task_scheduler_, then it will cause data race issues on the
+  // pointer.
+  SimpleTaskScheduler* task_scheduler_local = nullptr;
+  {
+    absl_ports::unique_lock l(&mutex_);
+
+    if (task_scheduler_ != nullptr) {
+      task_scheduler_local = task_scheduler_.release();
+    }
+  }
+
+  // Delete the task scheduler OUTSIDE of the critical section. Otherwise, it is
+  // possible that a background task starts to run and blocks at acquiring the
+  // global lock, which causes deadlock.
+  if (task_scheduler_local != nullptr) {
+    delete task_scheduler_local;
+  }
+}
+
+std::function<void()> IcingSearchEngine::CreateHandleExpiredDocumentsTask() {
+  return [this]() -> void {
+    HandleExpiredDocumentsResultProto result = HandleExpiredDocuments();
+    if (result.status().code() != StatusProto::OK) {
+      ICING_LOG(ERROR)
+          << "Failed to handle expired documents in the background: "
+          << result.status().message();
+    }
+  };
+}
+
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/icing-search-engine.h b/icing/icing-search-engine.h
index 1fc8e23..f1bb1af 100644
--- a/icing/icing-search-engine.h
+++ b/icing/icing-search-engine.h
@@ -16,6 +16,7 @@
 #define ICING_ICING_SEARCH_ENGINE_H_
 
 #include <cstdint>
+#include <functional>
 #include <memory>
 #include <string>
 #include <string_view>
@@ -63,7 +64,8 @@
 #include "icing/tokenization/language-segmenter.h"
 #include "icing/transform/normalizer.h"
 #include "icing/util/clock.h"
-#include "icing/util/icu-data-file-helper.h"
+#include "icing/util/simple-task-scheduler.h"
+#include "icing/util/tokenized-document.h"
 
 namespace icing {
 namespace lib {
@@ -321,6 +323,11 @@
   GetResultProto Get(std::string_view name_space, std::string_view uri,
                      const GetResultSpecProto& result_spec);
 
+  // Finds and returns the documents identified by the given GetResultSpecProto.
+  // Returns:
+  //   A BatchGetResultProto with a list of GetResultProto.
+  BatchGetResultProto BatchGet(GetResultSpecProto&& get_result_spec);
+
   // Reports usage. The corresponding usage scores of the specified document in
   // the report will be updated.
   //
@@ -440,6 +447,11 @@
   //   ABORTED if failed to get results but existing data is not affected
   //   FAILED_PRECONDITION IcingSearchEngine has not been initialized yet
   //   INTERNAL_ERROR on any other errors
+  SearchResultProto GetNextPage(GetNextPageRequestProto&& get_next_page_request)
+      ICING_LOCKS_EXCLUDED(mutex_);
+
+  // TODO: b/417644758 - Remove this method once all old callers are migrated to
+  // the new GetNextPage API. Internally, this should just be used in tests.
   SearchResultProto GetNextPage(uint64_t next_page_token)
       ICING_LOCKS_EXCLUDED(mutex_);
 
@@ -448,6 +460,28 @@
   void InvalidateNextPageToken(uint64_t next_page_token)
       ICING_LOCKS_EXCLUDED(mutex_);
 
+  // Handles expired documents.
+  // - Purges expired documents from the document store. Note: it also purges
+  //   documents that expire before
+  //   current_time_ms + options_.expired_document_purge_threshold_ms().
+  // - Propagates the deletion to their child documents.
+  // - Reschedules another (background) task to handle the next expired
+  //   document(s).
+  //
+  // This method is called by:
+  // - (Icing lib direct client only) Icing's internal task scheduler.
+  // - AppSearch task scheduler.
+  // - In the last step of Initialize(), to purge documents that expire during
+  //   Icing was off. Note: Initialize() calls the locked version of this
+  //   method.
+  //
+  // Returns a HandleExpiredDocumentsResultProto with status:
+  // - OK with results on success
+  // - FAILED_PRECONDITION_ERROR IcingSearchEngine has not been initialized yet
+  // - INTERNAL_ERROR on any other errors
+  HandleExpiredDocumentsResultProto HandleExpiredDocuments()
+      ICING_LOCKS_EXCLUDED(mutex_);
+
   // Gets or creates a file for write only purpose for the given blob handle.
   // To mark the blob is completed written, CommitBlob must be called. Once
   // CommitBlob is called, the blob is sealed and rewrite is not allowed.
@@ -524,6 +558,22 @@
   BlobProto CommitBlob(const PropertyProto::BlobHandleProto& blob_handle)
       ICING_LOCKS_EXCLUDED(mutex_);
 
+  // Gets all the blob info from the blob store.
+  //
+  // Returns:
+  //   BlobProto with all the blob info on success
+  //   InternalError on IO error
+  BlobProto GetAllBlobInfos() ICING_LOCKS_EXCLUDED(mutex_);
+
+  // Puts the blob info protos from the blob proto to the blob info proto log
+  // file.
+  //
+  // Returns:
+  //   BlobProto with all the blob info on success
+  //   InternalError on IO error
+  BlobProto PutBlobInfos(const BlobProto& blob_info_protos)
+      ICING_LOCKS_EXCLUDED(mutex_);
+
   // Makes sure that every update/delete received till this point is flushed
   // to disk. If the app crashes after a call to PersistToDisk(), Icing
   // would be able to fully recover all data written up to this point.
@@ -605,16 +655,24 @@
   //   INTERNAL_ERROR if internal state is no longer consistent
   ResetResultProto Reset() ICING_LOCKS_EXCLUDED(mutex_);
 
+  // Clears all data from Icing. Clients DO need to call Initialize again.
+  //
+  // Returns:
+  //   OK on success
+  //   INTERNAL_ERROR if failed to delete underlying files
+  ResetResultProto ClearAndDestroy() ICING_LOCKS_EXCLUDED(mutex_);
+
   // Disallow copy and move.
   IcingSearchEngine(const IcingSearchEngine&) = delete;
   IcingSearchEngine& operator=(const IcingSearchEngine&) = delete;
 
  protected:
-  IcingSearchEngine(IcingSearchEngineOptions options,
-                    std::unique_ptr<const Filesystem> filesystem,
-                    std::unique_ptr<const IcingFilesystem> icing_filesystem,
-                    std::unique_ptr<Clock> clock,
-                    std::unique_ptr<const JniCache> jni_cache = nullptr);
+  explicit IcingSearchEngine(
+      IcingSearchEngineOptions options,
+      std::unique_ptr<const Filesystem> filesystem,
+      std::unique_ptr<const IcingFilesystem> icing_filesystem,
+      std::unique_ptr<Clock> clock,
+      std::unique_ptr<const JniCache> jni_cache = nullptr);
 
  private:
   const IcingSearchEngineOptions options_;
@@ -673,15 +731,52 @@
   // Storage for all hits of embedding contents from the document store.
   std::unique_ptr<EmbeddingIndex> embedding_index_ ICING_GUARDED_BY(mutex_);
 
+  // Simple task scheduler for background tasks. Currently, it is used for:
+  // - Handle (purge) expired documents.
+  //
+  // Note: after acquiring the global lock, checking task_scheduler_ is nullptr
+  //   or not before accessing it **is ESSENTIAL**.
+  // - task_scheduler_ may be always nullptr if
+  //   options_.enable_background_task_scheduler() is false.
+  // - Race condition during destruction. Consider an example:
+  //   - Task scheduler thread: wakes up and ready to run
+  //     HandleExpiredDocuments()
+  //   - Main thread (which holds IcingSearchEngine object): destructs
+  //     IcingSearchEngine instance. IcingSearchEngine destructor destructs
+  //     task_scheduler_ via DestroyTaskScheduler() and wait to join the task
+  //     scheduler thread. At this point, task_scheduler_ is nullptr.
+  //   - Task scheduler thread: start to run HandleExpiredDocuments() and
+  //     reaches the point to reschedule the task.
+  //   - If task_scheduler_ is not checked, then the code will crash at runtime
+  //     because task_scheduler_ is already set to nullptr.
+  std::unique_ptr<SimpleTaskScheduler> task_scheduler_ ICING_GUARDED_BY(mutex_);
+
   // Pointer to JNI class references
   const std::unique_ptr<const JniCache> jni_cache_;
 
   // Resets all members that are created during Initialize.
-  void ResetMembers() ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+  //
+  // Note: this method DOES NOT reset task_scheduler_. task_scheduler_ should be
+  //   reset only if we want to destroy IcingSearchEngine, and in this case,
+  //   DestroyTaskScheduler should be called.
+  void ResetMembersLocked() ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
 
   // Resets all members that are created during Initialize, deletes all
   // underlying files and initializes a fresh index.
-  ResetResultProto ResetInternal() ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+  ResetResultProto ResetLocked() ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+
+  // Resets all members and clears all data from Icing.
+  ResetResultProto ClearAndDestroyLocked()
+      ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+
+  // Assumes mutex_ is already held.
+  PutResultProto PutLocked(DocumentProto&& document, int64_t current_time_ms)
+      ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+
+  // Assumes mutex_ is already held.
+  GetResultProto GetLocked(std::string_view name_space, std::string_view uri,
+    const GetResultSpecProto& result_spec)
+      ICING_SHARED_LOCKS_REQUIRED(mutex_);
 
   // Checks for the existence of the init marker file. If the failed init count
   // exceeds kMaxUnsuccessfulInitAttempts, all data is deleted and the index is
@@ -699,13 +794,22 @@
   // separate method so that other public methods don't need to call
   // PersistToDisk(). Public methods calling each other may cause deadlock
   // issues.
-  libtextclassifier3::Status InternalPersistToDisk(
-      PersistType::Code persist_type) ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+  //
+  // @param persist_type: The type of persistence guarantee that PersistToDisk
+  // should provide.
+  // @param persist_stats: The NON-null stats about the PersistToDisk call.
+  libtextclassifier3::Status PersistToDiskLocked(
+      PersistType::Code persist_type, PersistToDiskStatsProto* persist_stats)
+      ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
 
   // Helper method to the actual work to Initialize. We need this separate
   // method so that other public methods don't need to call Initialize(). Public
   // methods calling each other may cause deadlock issues.
-  InitializeResultProto InternalInitialize()
+  InitializeResultProto InitializeLocked()
+      ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+
+  // Helper method to the actual work to handle expired documents.
+  HandleExpiredDocumentsResultProto HandleExpiredDocumentsLocked()
       ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
 
   // Helper method to initialize member variables.
@@ -753,8 +857,8 @@
   //   OK on success
   //   FAILED_PRECONDITION if initialize_stats is null
   libtextclassifier3::Status InitializeBlobStore(
-      int32_t orphan_blob_time_to_live_ms, int32_t compression_level)
-      ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+      int32_t orphan_blob_time_to_live_ms, int32_t compression_level,
+      int32_t compression_mem_level) ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
 
   // Do any initialization/recovery necessary to create term index, integer
   // index, and qualified id join index instances.
@@ -792,11 +896,24 @@
 
   // Helper method for the actual work to Search. We need this separate
   // method to manage locking for Search.
-  SearchResultProto InternalSearch(const SearchSpecProto& search_spec,
+  SearchResultProto SearchLocked(const SearchSpecProto& search_spec,
                                    const ScoringSpecProto& scoring_spec,
                                    const ResultSpecProto& result_spec)
       ICING_SHARED_LOCKS_REQUIRED(mutex_);
 
+  // Helper method to prepare a document for indexing. This includes
+  // tokenization and dependency evaluation.
+  //
+  // TODO(b/397769319): change the input and output to a vector of documents to
+  //   support batch indexing.
+  //
+  // Returns:
+  //   On success, TokenizedDocument.
+  //   Any errors from tokenization or dependency evaluation.
+  libtextclassifier3::StatusOr<TokenizedDocument> PrepareDocumentsForIndexing(
+      DocumentProto&& document, int64_t current_time_ms)
+      ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+
   // Processes query and scores according to the specs. It is a helper function
   // (called by Search) to process and score normal query and the nested child
   // query for join search.
@@ -836,11 +953,12 @@
   // joinable properties with delete propagation enabled.
   //
   // Returns:
-  //   Number of propagated documents deleted on success
+  //   A list of metadata of the propagated documents deleted on success
   //   INTERNAL_ERROR on any I/O errors
-  libtextclassifier3::StatusOr<int> PropagateDelete(
-      const std::unordered_set<DocumentId>& deleted_document_ids,
-      int64_t current_time_ms) ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+  libtextclassifier3::StatusOr<std::vector<DocumentStore::DocumentMetadata>>
+  PropagateDelete(const std::unordered_set<DocumentId>& deleted_document_ids,
+                  int64_t current_time_ms)
+      ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
 
   // Discards derived data that requires rebuild based on rebuild_info.
   //
@@ -902,6 +1020,12 @@
       std::vector<std::unique_ptr<DataIndexingHandler>>>
   CreateDataIndexingHandlers() ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
 
+  // Destroys the task scheduler.
+  //
+  // Task scheduler should be destroyed ONLY IF we want to destroy
+  // IcingSearchEngine (e.g. destructor, ClearAndDestroy API, etc).
+  void DestroyTaskScheduler() ICING_LOCKS_EXCLUDED(mutex_);
+
   // Helper method to discard parts of (term, integer, qualified id join,
   // embedding) indices if they contain data for document ids greater than
   // last_stored_document_id.
@@ -1030,6 +1154,11 @@
   //   INTERNAL_ERROR on any I/O errors
   libtextclassifier3::Status ClearAllIndices()
       ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+
+  // Helper method to create a lambda function for handling expired documents
+  // task. This is used to schedule the task with the task scheduler.
+  std::function<void()> CreateHandleExpiredDocumentsTask()
+      ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
 };
 
 }  // namespace lib
diff --git a/icing/icing-search-engine_benchmark.cc b/icing/icing-search-engine_benchmark.cc
index 3061300..82fc9a6 100644
--- a/icing/icing-search-engine_benchmark.cc
+++ b/icing/icing-search-engine_benchmark.cc
@@ -14,18 +14,19 @@
 
 #include <unistd.h>
 
-#include <fstream>
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
 #include <iostream>
 #include <limits>
 #include <memory>
 #include <numeric>
 #include <ostream>
 #include <random>
-#include <sstream>
-#include <stdexcept>
 #include <string>
 #include <string_view>
 #include <unordered_set>
+#include <utility>
 #include <vector>
 
 #include "testing/base/public/benchmark.h"
@@ -45,7 +46,9 @@
 #include "icing/proto/status.pb.h"
 #include "icing/proto/term.pb.h"
 #include "icing/query/query-features.h"
+#include "icing/result/result-state-manager.h"
 #include "icing/schema-builder.h"
+#include "icing/store/document-id.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/document-generator.h"
 #include "icing/testing/numeric/number-generator.h"
@@ -150,6 +153,7 @@
   explicit DestructibleDirectory(const Filesystem& filesystem,
                                  const std::string& dir)
       : filesystem_(filesystem), dir_(dir) {
+    filesystem_.DeleteDirectoryRecursively(dir_.c_str());
     filesystem_.CreateDirectoryRecursively(dir_.c_str());
   }
   ~DestructibleDirectory() {
@@ -204,6 +208,18 @@
       /*range_upper=*/static_cast<int64_t>(num_documents) * 10 - 1);
 }
 
+PropertyProto::VectorProto GenerateRandomVector(
+    std::default_random_engine& random, const std::string& model_signature,
+    int dimension) {
+  std::uniform_real_distribution<float> dis(-1.0, 1.0);
+  PropertyProto::VectorProto vector;
+  vector.set_model_signature(model_signature);
+  for (int i = 0; i < dimension; ++i) {
+    vector.add_values(dis(random));
+  }
+  return vector;
+}
+
 void BM_IndexLatency(benchmark::State& state) {
   // Initialize the filesystem
   std::string test_dir = GetTestTempDir() + "/icing/benchmark";
@@ -222,7 +238,6 @@
   // Create the index.
   IcingSearchEngineOptions options;
   options.set_base_dir(test_dir);
-  options.set_index_merge_size(kIcingFullIndexSize);
   std::unique_ptr<IcingSearchEngine> icing =
       std::make_unique<IcingSearchEngine>(options);
 
@@ -291,6 +306,135 @@
     // Arguments: num_indexed_documents, num_sections
     ->ArgPair(1000000, 2);
 
+void BM_EmbeddingQueryLatency(benchmark::State& state) {
+  // Initialize the filesystem
+  std::string test_dir = GetTestTempDir() + "/icing/benchmark";
+  Filesystem filesystem;
+  DestructibleDirectory ddir(filesystem, test_dir);
+
+  std::default_random_engine random;
+
+  // Create the schema.
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Type1").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("embedding")
+                  .SetDataTypeVector(EMBEDDING_INDEXING_LINEAR_SEARCH)
+                  .SetCardinality(CARDINALITY_REPEATED)))
+          .AddType(SchemaTypeConfigBuilder().SetType("Type2").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("embedding")
+                  .SetDataTypeVector(EMBEDDING_INDEXING_LINEAR_SEARCH)
+                  .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+
+  // Create the index.
+  IcingSearchEngineOptions options;
+  options.set_base_dir(test_dir);
+  options.set_index_merge_size(kIcingFullIndexSize);
+  options.set_enable_passing_filter_to_children(true);
+  std::unique_ptr<IcingSearchEngine> icing =
+      std::make_unique<IcingSearchEngine>(options);
+
+  ASSERT_THAT(icing->Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing->SetSchema(schema).status(), ProtoIsOk());
+
+  int dimension = state.range(0);
+  int num_vectors_per_doc = state.range(1);
+  int num_type1_docs = state.range(2);
+  int num_type2_docs = state.range(3);
+  int filter_type = state.range(4);
+
+  // Type 1
+  for (int i = 0; i < num_type1_docs; ++i) {
+    DocumentProto doc = DocumentBuilder()
+                            .SetKey("icing", "type1" + std::to_string(i))
+                            .SetSchema("Type1")
+                            .SetCreationTimestampMs(1)
+                            .Build();
+    auto embedding_property = doc.add_properties();
+    embedding_property->set_name("embedding");
+    for (int j = 0; j < num_vectors_per_doc; ++j) {
+      embedding_property->mutable_vector_values()->Add(
+          GenerateRandomVector(random, /*model_signature=*/"model", dimension));
+    }
+    ASSERT_THAT(icing->Put(doc).status(), ProtoIsOk());
+  }
+
+  // Type 2
+  for (int i = 0; i < num_type2_docs; ++i) {
+    DocumentProto doc = DocumentBuilder()
+                            .SetKey("icing", "type2" + std::to_string(i))
+                            .SetSchema("Type2")
+                            .SetCreationTimestampMs(1)
+                            .Build();
+    auto embedding_property = doc.add_properties();
+    embedding_property->set_name("embedding");
+    for (int j = 0; j < num_vectors_per_doc; ++j) {
+      embedding_property->mutable_vector_values()->Add(
+          GenerateRandomVector(random, /*model_signature=*/"model", dimension));
+    }
+    ASSERT_THAT(icing->Put(doc).status(), ProtoIsOk());
+  }
+
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::EXACT_ONLY);
+  search_spec.set_embedding_query_metric_type(
+      SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT);
+  search_spec.add_enabled_features(
+      std::string(kListFilterQueryLanguageFeature));
+  *search_spec.add_embedding_query_vectors() =
+      GenerateRandomVector(random, /*model_signature=*/"model", dimension);
+  search_spec.set_query("semanticSearch(getEmbeddingParameter(0))");
+
+  int expected_num_docs_scored = 0;
+  if (filter_type == 1) {
+    search_spec.add_schema_type_filters("Type1");
+    expected_num_docs_scored = num_type1_docs;
+  } else if (filter_type == 2) {
+    search_spec.add_schema_type_filters("Type2");
+    expected_num_docs_scored = num_type2_docs;
+  } else {
+    expected_num_docs_scored = num_type1_docs + num_type2_docs;
+  }
+
+  ScoringSpecProto scoring_spec = CreateScoringSpec(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  scoring_spec.set_advanced_scoring_expression(
+      "len(this.matchedSemanticScores(getEmbeddingParameter(0)))");
+  ResultSpecProto result_spec = ResultSpecProto::default_instance();
+  result_spec.set_num_to_score(10000000);
+
+  std::string scoring_latency_ms_str;
+  for (auto _ : state) {
+    SearchResultProto results =
+        icing->Search(search_spec, scoring_spec, result_spec);
+
+    ASSERT_THAT(results.status(), ProtoIsOk());
+    for (const SearchResultProto::ResultProto& result : results.results()) {
+      ASSERT_THAT(result.score(), num_vectors_per_doc);
+    }
+    ASSERT_THAT(
+        results.query_stats().parent_search_stats().num_documents_scored(),
+        Eq(expected_num_docs_scored));
+    int scoring_latency_ms =
+        results.query_stats().parent_search_stats().scoring_latency_ms();
+    scoring_latency_ms_str += std::to_string(scoring_latency_ms) + ", ";
+  }
+  state.SetLabel("Scoring latency ms: " + scoring_latency_ms_str);
+}
+BENCHMARK(BM_EmbeddingQueryLatency)
+    // Arguments:
+    // - dimension
+    // - num_vectors_per_doc
+    // - num_type1_docs,
+    // - num_type2_docs
+    // - filter_type (1: filter by type1, 2: filter by type2, Other: no filter)
+    ->Args({768, 2, 4000, 25000, 0})
+    ->Args({768, 2, 4000, 25000, 1})
+    ->Args({768, 2, 4000, 25000, 2});
+
 void BM_IndexThroughput(benchmark::State& state) {
   // Initialize the filesystem
   std::string test_dir = GetTestTempDir() + "/icing/benchmark";
@@ -1205,6 +1349,83 @@
 }
 BENCHMARK(BM_JoinQueryQualifiedId);
 
+void BM_Optimize(benchmark::State& state) {
+  int num_docs = state.range(0);
+  int num_deleted = state.range(1);
+
+  // Initialize the filesystem
+  std::string test_dir = GetTestTempDir() + "/icing/benchmark";
+  Filesystem filesystem;
+
+  // Create the schema.
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Message").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("body")
+                  .SetDataTypeString(TermMatchType::PREFIX,
+                                     StringIndexingConfig::TokenizerType::PLAIN)
+                  .SetCardinality(PropertyConfigProto::Cardinality::OPTIONAL)))
+          .Build();
+
+  // Create documents.
+  DocumentProto base_document = DocumentBuilder()
+                                    .SetSchema("Message")
+                                    .SetNamespace("namespace")
+                                    .AddStringProperty("body", "foo")
+                                    .Build();
+  std::vector<std::string> uris;
+  uris.reserve(num_docs);
+  std::vector<DocumentProto> documents;
+  documents.reserve(num_docs);
+  for (int i = 0; i < num_docs; ++i) {
+    std::string uri = std::to_string(i);
+    uris.push_back(uri);
+    documents.push_back(DocumentBuilder(base_document).SetUri(uri).Build());
+  }
+
+  std::vector<std::string> uris_to_delete = uris;
+  std::shuffle(uris_to_delete.begin(), uris_to_delete.end(),
+               std::default_random_engine(12345));
+  uris_to_delete.resize(num_deleted);
+
+  for (auto s : state) {
+    state.PauseTiming();
+    DestructibleDirectory ddir(filesystem, test_dir);
+    // Create the index.
+    IcingSearchEngineOptions options;
+    options.set_enable_optimize_improvements(true);
+    options.set_base_dir(test_dir);
+    options.set_index_merge_size(kIcingFullIndexSize);
+    std::unique_ptr<IcingSearchEngine> icing =
+        std::make_unique<IcingSearchEngine>(options);
+
+    ASSERT_THAT(icing->Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing->SetSchema(schema).status(), ProtoIsOk());
+
+    for (const auto& doc : documents) {
+      ASSERT_THAT(icing->Put(doc).status(), ProtoIsOk());
+    }
+
+    for (int i = 0; i < num_deleted; ++i) {
+      ASSERT_THAT(icing->Delete("namespace", uris_to_delete[i]).status(),
+                  ProtoIsOk());
+    }
+    state.ResumeTiming();
+
+    ASSERT_THAT(icing->Optimize().status(), ProtoIsOk());
+  }
+}
+
+BENCHMARK(BM_Optimize)
+    // Arguments: num_documents, num_deleted
+    ->ArgPair(1024, 256)
+    ->ArgPair(1024, 512)
+    ->ArgPair(1024, 1024)
+    ->ArgPair(8192, 2048)
+    ->ArgPair(8192, 4096)
+    ->ArgPair(8192, 8192);
+
 void BM_PersistToDisk(benchmark::State& state) {
   // Initialize the filesystem
   std::string test_dir = GetTestTempDir() + "/icing/benchmark";
diff --git a/icing/icing-search-engine_blob_test.cc b/icing/icing-search-engine_blob_test.cc
index b13d258..655c5c6 100644
--- a/icing/icing-search-engine_blob_test.cc
+++ b/icing/icing-search-engine_blob_test.cc
@@ -47,6 +47,7 @@
 namespace {
 
 using ::icing::lib::portable_equals_proto::EqualsProto;
+using ::testing::Eq;
 using ::testing::IsEmpty;
 using ::testing::SizeIs;
 using ::testing::UnorderedElementsAre;
@@ -234,13 +235,96 @@
     uint64_t size = filesystem()->GetFileSize(*read_fd);
     std::unique_ptr<unsigned char[]> buf =
         std::make_unique<unsigned char[]>(size);
-    EXPECT_TRUE(filesystem()->Read(read_fd.get(), buf.get(), size));
+    EXPECT_THAT(filesystem()->Read(read_fd.get(), buf.get(), size), Eq(size));
     std::string expected_data = std::string(data.begin(), data.end());
     std::string actual_data = std::string(buf.get(), buf.get() + size);
     EXPECT_EQ(expected_data, actual_data);
   }
 }
 
+TEST_P(IcingSearchEngineBlobTest, GetAllBlobInfo) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  PropertyProto::BlobHandleProto blob_handle;
+  std::vector<unsigned char> data = GenerateRandomBytes(24);
+  std::array<uint8_t, 32> digest = CalculateDigest(data);
+  blob_handle.set_digest((void*)digest.data(), digest.size());
+  blob_handle.set_namespace_("namespaceA");
+
+  BlobProto write_blob_proto = icing.OpenWriteBlob(blob_handle);
+  ASSERT_THAT(write_blob_proto.status(), ProtoIsOk());
+  {
+    ScopedFd write_fd(GetScopedFdFromBlobProto(write_blob_proto));
+    ASSERT_TRUE(filesystem()->Write(write_fd.get(), data.data(), data.size()));
+  }
+
+  BlobProto commit_blob_proto = icing.CommitBlob(blob_handle);
+  ASSERT_THAT(commit_blob_proto.status(), ProtoIsOk());
+
+  BlobProto blob_proto = icing.GetAllBlobInfos();
+  EXPECT_THAT(blob_proto.status(), ProtoIsOk());
+  EXPECT_THAT(blob_proto.blob_info_protos_size(), Eq(1));
+  EXPECT_THAT(blob_proto.blob_info_protos(0).blob_handle().namespace_(),
+              Eq("namespaceA"));
+}
+
+TEST_P(IcingSearchEngineBlobTest, PutBlobInfos) {
+  bool manage_blob_files = GetParam();
+
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  PropertyProto::BlobHandleProto blob_handle;
+  std::vector<unsigned char> data = GenerateRandomBytes(24);
+  std::array<uint8_t, 32> digest = CalculateDigest(data);
+  blob_handle.set_digest((void*)digest.data(), digest.size());
+  blob_handle.set_namespace_("namespaceA");
+
+  BlobProto write_blob_proto = icing.OpenWriteBlob(blob_handle);
+  ASSERT_THAT(write_blob_proto.status(), ProtoIsOk());
+  {
+    ScopedFd write_fd(GetScopedFdFromBlobProto(write_blob_proto));
+    ASSERT_TRUE(filesystem()->Write(write_fd.get(), data.data(), data.size()));
+  }
+
+  BlobProto commit_blob_proto = icing.CommitBlob(blob_handle);
+  ASSERT_THAT(commit_blob_proto.status(), ProtoIsOk());
+
+  BlobProto blob_proto = icing.GetAllBlobInfos();
+  EXPECT_THAT(blob_proto.status(), ProtoIsOk());
+  EXPECT_THAT(blob_proto.blob_info_protos_size(), Eq(1));
+  EXPECT_THAT(blob_proto.blob_info_protos(0).blob_handle().namespace_(),
+              Eq("namespaceA"));
+
+  // Delete the blob info proto log file.
+  filesystem()->DeleteFile(
+      absl_ports::StrCat(GetTestBlobDir(), "/blob_info_proto_file").c_str());
+
+  if (manage_blob_files) {
+    ASSERT_THAT(icing.PutBlobInfos(std::move(blob_proto)).status(),
+                ProtoStatusIs(StatusProto::FAILED_PRECONDITION));
+  } else {
+    // Put the blob info proto log file.
+    BlobProto result_blob_proto = icing.PutBlobInfos(std::move(blob_proto));
+    EXPECT_THAT(result_blob_proto.status(), ProtoIsOk());
+
+    BlobProto read_blob_proto = icing.OpenReadBlob(blob_handle);
+    ASSERT_THAT(read_blob_proto.status(), ProtoIsOk());
+    {
+      ScopedFd read_fd(GetScopedFdFromBlobProto(read_blob_proto));
+
+      uint64_t size = filesystem()->GetFileSize(*read_fd);
+      std::unique_ptr<unsigned char[]> buf =
+          std::make_unique<unsigned char[]>(size);
+      EXPECT_THAT(filesystem()->Read(read_fd.get(), buf.get(), size), Eq(size));
+      std::string expected_data = std::string(data.begin(), data.end());
+      std::string actual_data = std::string(buf.get(), buf.get() + size);
+      EXPECT_EQ(expected_data, actual_data);
+    }
+  }
+}
+
 TEST_P(IcingSearchEngineBlobTest, RemovePendingBlob) {
   bool manage_blob_files = GetParam();
 
@@ -372,7 +456,7 @@
 
     uint64_t size = filesystem()->GetFileSize(*read_fd);
     std::unique_ptr<uint8_t[]> buf = std::make_unique<uint8_t[]>(size);
-    EXPECT_TRUE(filesystem()->Read(read_fd.get(), buf.get(), size));
+    EXPECT_THAT(filesystem()->Read(read_fd.get(), buf.get(), size), Eq(size));
 
     std::string expected_data = std::string(data.begin(), data.end());
     std::string actual_data = std::string(buf.get(), buf.get() + size);
@@ -478,7 +562,7 @@
     ScopedFd read_fd(GetScopedFdFromBlobProto(read_blob_proto));
     uint64_t size = filesystem()->GetFileSize(*read_fd);
     std::unique_ptr<uint8_t[]> buf = std::make_unique<uint8_t[]>(size);
-    EXPECT_TRUE(filesystem()->Read(read_fd.get(), buf.get(), size));
+    EXPECT_THAT(filesystem()->Read(read_fd.get(), buf.get(), size), Eq(size));
     std::string expected_data = std::string(data.begin(), data.end());
     std::string actual_data = std::string(buf.get(), buf.get() + size);
     EXPECT_EQ(expected_data, actual_data);
@@ -522,7 +606,7 @@
     ScopedFd read_fd(GetScopedFdFromBlobProto(read_blob_proto));
     uint64_t size = filesystem()->GetFileSize(*read_fd);
     std::unique_ptr<uint8_t[]> buf = std::make_unique<uint8_t[]>(size);
-    EXPECT_TRUE(filesystem()->Read(read_fd.get(), buf.get(), size));
+    EXPECT_THAT(filesystem()->Read(read_fd.get(), buf.get(), size), Eq(size));
     std::string expected_data = std::string(data.begin(), data.end());
     std::string actual_data = std::string(buf.get(), buf.get() + size);
     EXPECT_EQ(expected_data, actual_data);
@@ -585,8 +669,7 @@
 
   uint64_t size = filesystem()->GetFileSize(*read_fd);
   std::unique_ptr<uint8_t[]> buf = std::make_unique<uint8_t[]>(size);
-  filesystem()->Read(read_fd.get(), buf.get(), size);
-  close(read_fd.get());
+  EXPECT_THAT(filesystem()->Read(read_fd.get(), buf.get(), size), Eq(size));
 
   std::string expected_data = std::string(data.begin(), data.end());
   std::string actual_data = std::string(buf.get(), buf.get() + size);
@@ -699,7 +782,6 @@
 
   ScopedFd write_fd(GetScopedFdFromBlobProto(writeBlobProto));
   ASSERT_TRUE(filesystem()->Write(write_fd.get(), data.data(), data.size()));
-  close(write_fd.get());
 
   BlobProto commitBlobProto = icing.CommitBlob(blob_handle);
   ASSERT_THAT(commitBlobProto.status(), ProtoIsOk());
@@ -731,7 +813,7 @@
     ScopedFd read_fd(GetScopedFdFromBlobProto(readBlobProto));
     uint64_t size = filesystem()->GetFileSize(*read_fd);
     std::unique_ptr<uint8_t[]> buf = std::make_unique<uint8_t[]>(size);
-    ASSERT_TRUE(filesystem()->Read(read_fd.get(), buf.get(), size));
+    ASSERT_THAT(filesystem()->Read(read_fd.get(), buf.get(), size), Eq(size));
 
     std::string expected_data = std::string(data.begin(), data.end());
     std::string actual_data = std::string(buf.get(), buf.get() + size);
@@ -748,7 +830,7 @@
 
     uint64_t size = filesystem()->GetFileSize(*read_fd2);
     std::unique_ptr<uint8_t[]> buf = std::make_unique<uint8_t[]>(size);
-    ASSERT_TRUE(filesystem()->Read(read_fd2.get(), buf.get(), size));
+    ASSERT_THAT(filesystem()->Read(read_fd2.get(), buf.get(), size), Eq(size));
 
     std::string expected_data = std::string(data.begin(), data.end());
     std::string actual_data = std::string(buf.get(), buf.get() + size);
@@ -781,7 +863,6 @@
 
   ScopedFd write_fd(GetScopedFdFromBlobProto(writeBlobProto));
   ASSERT_TRUE(filesystem()->Write(write_fd.get(), data.data(), data.size()));
-  close(write_fd.get());
 
   BlobProto commitBlobProto = icing.CommitBlob(blob_handle);
   ASSERT_THAT(commitBlobProto.status(), ProtoIsOk());
@@ -842,7 +923,7 @@
     ScopedFd read_fd(GetScopedFdFromBlobProto(readBlobProto));
     uint64_t size = filesystem()->GetFileSize(*read_fd);
     std::unique_ptr<uint8_t[]> buf = std::make_unique<uint8_t[]>(size);
-    ASSERT_TRUE(filesystem()->Read(read_fd.get(), buf.get(), size));
+    ASSERT_THAT(filesystem()->Read(read_fd.get(), buf.get(), size), Eq(size));
 
     std::string expected_data = std::string(data.begin(), data.end());
     std::string actual_data = std::string(buf.get(), buf.get() + size);
@@ -859,7 +940,7 @@
 
     uint64_t size = filesystem()->GetFileSize(*read_fd2);
     std::unique_ptr<uint8_t[]> buf = std::make_unique<uint8_t[]>(size);
-    ASSERT_TRUE(filesystem()->Read(read_fd2.get(), buf.get(), size));
+    ASSERT_THAT(filesystem()->Read(read_fd2.get(), buf.get(), size), Eq(size));
 
     std::string expected_data = std::string(data.begin(), data.end());
     std::string actual_data = std::string(buf.get(), buf.get() + size);
@@ -931,8 +1012,7 @@
 
     uint64_t size = filesystem()->GetFileSize(*read_fd);
     std::unique_ptr<uint8_t[]> buf = std::make_unique<uint8_t[]>(size);
-    filesystem()->Read(read_fd.get(), buf.get(), size);
-    close(read_fd.get());
+    EXPECT_THAT(filesystem()->Read(read_fd.get(), buf.get(), size), Eq(size));
 
     std::string expected_data = std::string(data.begin(), data.end());
     std::string actual_data = std::string(buf.get(), buf.get() + size);
@@ -951,8 +1031,7 @@
 
     uint64_t size = filesystem()->GetFileSize(*read_fd2);
     std::unique_ptr<uint8_t[]> buf = std::make_unique<uint8_t[]>(size);
-    filesystem()->Read(read_fd2.get(), buf.get(), size);
-    close(read_fd2.get());
+    EXPECT_THAT(filesystem()->Read(read_fd2.get(), buf.get(), size), Eq(size));
 
     std::string expected_data = std::string(data.begin(), data.end());
     std::string actual_data = std::string(buf.get(), buf.get() + size);
@@ -1160,8 +1239,7 @@
 
   uint64_t size = filesystem()->GetFileSize(*read_fd);
   std::unique_ptr<uint8_t[]> buf = std::make_unique<uint8_t[]>(size);
-  filesystem()->Read(read_fd.get(), buf.get(), size);
-  close(read_fd.get());
+  EXPECT_THAT(filesystem()->Read(read_fd.get(), buf.get(), size), Eq(size));
 
   std::string expected_data = std::string(data.begin(), data.end());
   std::string actual_data = std::string(buf.get(), buf.get() + size);
diff --git a/icing/icing-search-engine_delete_test.cc b/icing/icing-search-engine_delete_test.cc
index a7e370c..13b6a2f 100644
--- a/icing/icing-search-engine_delete_test.cc
+++ b/icing/icing-search-engine_delete_test.cc
@@ -12,10 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include <chrono>  // NOLINT
 #include <cstdint>
 #include <limits>
 #include <memory>
 #include <string>
+#include <thread>  // NOLINT
 #include <utility>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
@@ -58,11 +60,13 @@
 namespace {
 
 using ::icing::lib::portable_equals_proto::EqualsProto;
+using ::testing::AllOf;
 using ::testing::Eq;
 using ::testing::Ge;
 using ::testing::Gt;
 using ::testing::HasSubstr;
 using ::testing::IsEmpty;
+using ::testing::Property;
 using ::testing::Return;
 using ::testing::SizeIs;
 using ::testing::StrEq;
@@ -119,6 +123,7 @@
   IcingSearchEngineOptions icing_options;
   icing_options.set_base_dir(GetTestBaseDir());
   icing_options.set_enable_qualified_id_join_index_v3(true);
+  icing_options.set_enable_soft_index_restoration(true);
   icing_options.set_enable_delete_propagation_from(false);
   return icing_options;
 }
@@ -1155,6 +1160,689 @@
               EqualsProto(expected_get_result_proto6));
 }
 
+TEST_F(IcingSearchEngineDeleteTest,
+       HandleExpiredDocuments_taskSchedulerDisabled) {
+  SchemaProto schema =
+      SchemaBuilder().AddType(CreateMessageSchemaTypeConfig()).Build();
+
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace", "uri")
+                               .SetSchema("Message")
+                               .SetCreationTimestampMs(10)
+                               .SetTtlMs(1000)  // Expired at 1010 ms.
+                               .AddStringProperty("body", "message body1")
+                               .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_background_task_scheduler(false);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  auto fake_clock = std::make_unique<FakeClock>();
+  FakeClock* fake_clock_ptr = fake_clock.get();
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  InitializeResultProto initialize_result = icing.Initialize();
+  ASSERT_THAT(initialize_result.status(), ProtoIsOk());
+  EXPECT_THAT(
+      initialize_result.initialize_stats().next_expiration_timestamp_ms(),
+      Eq(-1));  // No next expiration timestamp since the database is empty.
+
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document).status(), ProtoIsOk());
+
+  // Sanity check that the document is present.
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto1.mutable_document() = document;
+  ASSERT_THAT(
+      icing.Get("namespace", "uri", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto1));
+
+  // Adjust the clock to 500 ms and call HandleExpiredDocuments. The document
+  // should still be present.
+  fake_clock_ptr->SetSystemTimeMilliseconds(500);
+  HandleExpiredDocumentsResultProto result_proto1 =
+      icing.HandleExpiredDocuments();
+  EXPECT_THAT(result_proto1.status(), ProtoIsOk());
+  EXPECT_THAT(result_proto1.num_expired_documents(), Eq(0));
+  EXPECT_THAT(result_proto1.num_propagated_deleted_documents(), Eq(0));
+  EXPECT_THAT(result_proto1.deleted_documents(), IsEmpty());
+  EXPECT_THAT(result_proto1.next_expiration_timestamp_ms(), Eq(1010));
+  EXPECT_THAT(
+      icing.Get("namespace", "uri", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto1));
+
+  // Adjust the clock to 1010 ms and call HandleExpiredDocuments. The document
+  // should be deleted.
+  fake_clock_ptr->SetSystemTimeMilliseconds(1010);
+  HandleExpiredDocumentsResultProto result_google::protobuf =
+      icing.HandleExpiredDocuments();
+  EXPECT_THAT(result_google::protobuf.status(), ProtoIsOk());
+  EXPECT_THAT(result_google::protobuf.num_expired_documents(), Eq(1));
+  EXPECT_THAT(result_google::protobuf.num_propagated_deleted_documents(), Eq(0));
+  EXPECT_THAT(
+      result_google::protobuf.deleted_documents(),
+      UnorderedElementsAre(AllOf(
+          Property(
+              &HandleExpiredDocumentsResultProto::DocumentGroupInfo::name_space,
+              Eq("namespace")),
+          Property(
+              &HandleExpiredDocumentsResultProto::DocumentGroupInfo::schema,
+              Eq("Message")),
+          Property(&HandleExpiredDocumentsResultProto::DocumentGroupInfo::uris,
+                   UnorderedElementsAre("uri")))));
+  EXPECT_THAT(result_google::protobuf.next_expiration_timestamp_ms(), Eq(-1));
+
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_google::protobuf.mutable_status()->set_message(
+      "Document (namespace, uri) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "uri", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_google::protobuf));
+
+  // Adjust the clock back to 500 ms and get the document again. Should get
+  // NOT_FOUND error.
+  // This is a hack to make sure that the document is "purged" when we handle
+  // expired documents instead of just marking deleted.
+  fake_clock_ptr->SetSystemTimeMilliseconds(500);
+  EXPECT_THAT(
+      icing.Get("namespace", "uri", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_google::protobuf));
+}
+
+TEST_F(
+    IcingSearchEngineDeleteTest,
+    HandleExpiredDocuments_taskSchedulerDisabled_propagateToChildrenWithDeletePropagationEnabled) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Message")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("body")
+                                        .SetDataTypeString(TERM_MATCH_PREFIX,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("sender")
+                                        .SetDataTypeJoinableString(
+                                            JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                            DELETE_PROPAGATION_TYPE_NONE)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Label")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("name")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_REQUIRED))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("target")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto person1 = DocumentBuilder()
+                              .SetKey("namespace", "person1")
+                              .SetSchema("Person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(1000)  // Expired at 1010 ms.
+                              .AddStringProperty("name", "Alice")
+                              .Build();
+  DocumentProto person2 = DocumentBuilder()
+                              .SetKey("namespace", "person2")
+                              .SetSchema("Person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(3000)  // Expired at 3010 ms.
+                              .AddStringProperty("name", "Bob")
+                              .Build();
+  DocumentProto email1 = DocumentBuilder()
+                             .SetKey("namespace", "email1")
+                             .SetSchema("Email")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("subject", "test")
+                             .AddStringProperty("sender", "namespace#person1")
+                             .Build();
+  DocumentProto email2 = DocumentBuilder()
+                             .SetKey("namespace", "email2")
+                             .SetSchema("Email")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("subject", "test")
+                             .AddStringProperty("sender", "namespace#person2")
+                             .Build();
+  DocumentProto message1 = DocumentBuilder()
+                               .SetKey("namespace", "message1")
+                               .SetSchema("Message")
+                               .SetCreationTimestampMs(10)
+                               .SetTtlMs(0)  // Never expire.
+                               .AddStringProperty("body", "test")
+                               .AddStringProperty("sender", "namespace#person1")
+                               .Build();
+  DocumentProto message2 = DocumentBuilder()
+                               .SetKey("namespace", "message2")
+                               .SetSchema("Message")
+                               .SetCreationTimestampMs(10)
+                               .SetTtlMs(0)  // Never expire.
+                               .AddStringProperty("body", "test")
+                               .AddStringProperty("sender", "namespace#person2")
+                               .Build();
+  DocumentProto label1 = DocumentBuilder()
+                             .SetKey("namespace", "label1")
+                             .SetSchema("Label")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("name", "label1")
+                             .AddStringProperty("target", "namespace#email1")
+                             .Build();
+  DocumentProto label2 = DocumentBuilder()
+                             .SetKey("namespace", "label2")
+                             .SetSchema("Label")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("name", "label2")
+                             .AddStringProperty("target", "namespace#email2")
+                             .Build();
+  DocumentProto label3 = DocumentBuilder()
+                             .SetKey("namespace", "label3")
+                             .SetSchema("Label")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("name", "label3")
+                             .AddStringProperty("target", "namespace#message1")
+                             .Build();
+  DocumentProto label4 = DocumentBuilder()
+                             .SetKey("namespace", "label4")
+                             .SetSchema("Label")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("name", "label4")
+                             .AddStringProperty("target", "namespace#message2")
+                             .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_background_task_scheduler(false);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  auto fake_clock = std::make_unique<FakeClock>();
+  FakeClock* fake_clock_ptr = fake_clock.get();
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  InitializeResultProto initialize_result = icing.Initialize();
+  ASSERT_THAT(initialize_result.status(), ProtoIsOk());
+  EXPECT_THAT(
+      initialize_result.initialize_stats().next_expiration_timestamp_ms(),
+      Eq(-1));  // No next expiration timestamp since the database is empty.
+
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(person1).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(person2).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email1).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email2).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(message1).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(message2).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(label1).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(label2).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(label3).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(label4).status(), ProtoIsOk());
+
+  // Adjust the clock to 1010 ms and call HandleExpiredDocuments. person1 should
+  // be deleted and delete propagation should be triggered for email1 (child)
+  // and label1 (grandchild).
+  fake_clock_ptr->SetSystemTimeMilliseconds(1010);
+  HandleExpiredDocumentsResultProto result_proto =
+      icing.HandleExpiredDocuments();
+  EXPECT_THAT(result_proto.status(), ProtoIsOk());
+  EXPECT_THAT(result_proto.num_expired_documents(), Eq(1));
+  EXPECT_THAT(result_proto.num_propagated_deleted_documents(), Eq(2));
+  EXPECT_THAT(
+      result_proto.deleted_documents(),
+      UnorderedElementsAre(
+          AllOf(
+              Property(&HandleExpiredDocumentsResultProto::DocumentGroupInfo::
+                           name_space,
+                       Eq("namespace")),
+              Property(
+                  &HandleExpiredDocumentsResultProto::DocumentGroupInfo::schema,
+                  Eq("Person")),
+              Property(
+                  &HandleExpiredDocumentsResultProto::DocumentGroupInfo::uris,
+                  UnorderedElementsAre("person1"))),
+          AllOf(
+              Property(&HandleExpiredDocumentsResultProto::DocumentGroupInfo::
+                           name_space,
+                       Eq("namespace")),
+              Property(
+                  &HandleExpiredDocumentsResultProto::DocumentGroupInfo::schema,
+                  Eq("Email")),
+              Property(
+                  &HandleExpiredDocumentsResultProto::DocumentGroupInfo::uris,
+                  UnorderedElementsAre("email1"))),
+          AllOf(
+              Property(&HandleExpiredDocumentsResultProto::DocumentGroupInfo::
+                           name_space,
+                       Eq("namespace")),
+              Property(
+                  &HandleExpiredDocumentsResultProto::DocumentGroupInfo::schema,
+                  Eq("Label")),
+              Property(
+                  &HandleExpiredDocumentsResultProto::DocumentGroupInfo::uris,
+                  UnorderedElementsAre("label1")))));
+  EXPECT_THAT(result_proto.next_expiration_timestamp_ms(), Eq(3010));
+
+  // Adjust the clock back to 500 ms and verify Get API for email, message and
+  // label documents.
+  // This is a hack to make sure that the document is "purged" when we handle
+  // expired documents instead of just marking deleted.
+  fake_clock_ptr->SetSystemTimeMilliseconds(500);
+
+  // Email1 should be deleted. The joinable property "sender" in schema type
+  // "Email" has delete propagation type PROPAGATE_FROM and the referenced
+  // document "person1" is deleted.
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto1.mutable_status()->set_message(
+      "Document (namespace, email1) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "email1", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto1));
+
+  // Email2 should still exist. The joinable property "sender" in schema type
+  // "Email" has delete propagation type PROPAGATE_FROM but the referenced
+  // document "person2" is not deleted.
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_google::protobuf.mutable_document() = email2;
+  EXPECT_THAT(
+      icing.Get("namespace", "email2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_google::protobuf));
+
+  // Message1 should still exist. The joinable property "sender" in schema type
+  // "Message" has delete propagation type NONE.
+  GetResultProto expected_get_result_proto3;
+  expected_get_result_proto3.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto3.mutable_document() = message1;
+  EXPECT_THAT(icing.Get("namespace", "message1",
+                        GetResultSpecProto::default_instance()),
+              EqualsProto(expected_get_result_proto3));
+
+  // Message2 should still exist. The joinable property "sender" in schema type
+  // "Message" has delete propagation type NONE, and the referenced document
+  // "person2" is not deleted.
+  GetResultProto expected_get_result_proto4;
+  expected_get_result_proto4.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto4.mutable_document() = message2;
+  EXPECT_THAT(icing.Get("namespace", "message2",
+                        GetResultSpecProto::default_instance()),
+              EqualsProto(expected_get_result_proto4));
+
+  // Label1 should be deleted (propagated from email1).
+  GetResultProto expected_get_result_proto5;
+  expected_get_result_proto5.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto5.mutable_status()->set_message(
+      "Document (namespace, label1) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "label1", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto5));
+
+  // Label2 should still exist.
+  GetResultProto expected_get_result_proto6;
+  expected_get_result_proto6.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto6.mutable_document() = label2;
+  EXPECT_THAT(
+      icing.Get("namespace", "label2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto6));
+
+  // Label3 should still exist.
+  GetResultProto expected_get_result_proto7;
+  expected_get_result_proto7.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto7.mutable_document() = label3;
+  EXPECT_THAT(
+      icing.Get("namespace", "label3", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto7));
+
+  // Label4 should still exist.
+  GetResultProto expected_get_result_proto8;
+  expected_get_result_proto8.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto8.mutable_document() = label4;
+  EXPECT_THAT(
+      icing.Get("namespace", "label4", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto8));
+}
+
+TEST_F(
+    IcingSearchEngineDeleteTest,
+    HandleExpiredDocuments_taskSchedulerDisabled_shouldPurgeDocumentsThatExpireWithinThreshold) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto person1 = DocumentBuilder()
+                              .SetKey("namespace", "person1")
+                              .SetSchema("Person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(1000)  // Expired at 1010 ms.
+                              .AddStringProperty("name", "Alice")
+                              .Build();
+  DocumentProto person2 = DocumentBuilder()
+                              .SetKey("namespace", "person2")
+                              .SetSchema("Person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(1101)  // Expired at 1111 ms.
+                              .AddStringProperty("name", "Bob")
+                              .Build();
+  DocumentProto person3 = DocumentBuilder()
+                              .SetKey("namespace", "person3")
+                              .SetSchema("Person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(1100)  // Expired at 1110 ms.
+                              .AddStringProperty("name", "Bob")
+                              .Build();
+  DocumentProto email1 = DocumentBuilder()
+                             .SetKey("namespace", "email1")
+                             .SetSchema("Email")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("subject", "test")
+                             .AddStringProperty("sender", "namespace#person1")
+                             .Build();
+  DocumentProto email2 = DocumentBuilder()
+                             .SetKey("namespace", "email2")
+                             .SetSchema("Email")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("subject", "test")
+                             .AddStringProperty("sender", "namespace#person2")
+                             .Build();
+  DocumentProto email3 = DocumentBuilder()
+                             .SetKey("namespace", "email3")
+                             .SetSchema("Email")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("subject", "test")
+                             .AddStringProperty("sender", "namespace#person3")
+                             .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_background_task_scheduler(false);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(100);
+
+  auto fake_clock = std::make_unique<FakeClock>();
+  FakeClock* fake_clock_ptr = fake_clock.get();
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  InitializeResultProto initialize_result = icing.Initialize();
+  ASSERT_THAT(initialize_result.status(), ProtoIsOk());
+  EXPECT_THAT(
+      initialize_result.initialize_stats().next_expiration_timestamp_ms(),
+      Eq(-1));  // No next expiration timestamp since the database is empty.
+
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(person1).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(person2).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(person3).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email1).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email2).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email3).status(), ProtoIsOk());
+
+  // Adjust the clock to 1010 ms and call HandleExpiredDocuments. person1 and
+  // person3 should be deleted, and delete propagation should be triggered for
+  // email1 and email3.
+  fake_clock_ptr->SetSystemTimeMilliseconds(1010);
+  HandleExpiredDocumentsResultProto result_proto =
+      icing.HandleExpiredDocuments();
+  EXPECT_THAT(result_proto.status(), ProtoIsOk());
+  EXPECT_THAT(result_proto.num_expired_documents(), Eq(2));
+  EXPECT_THAT(result_proto.num_propagated_deleted_documents(), Eq(2));
+  EXPECT_THAT(
+      result_proto.deleted_documents(),
+      UnorderedElementsAre(
+          AllOf(
+              Property(&HandleExpiredDocumentsResultProto::DocumentGroupInfo::
+                           name_space,
+                       Eq("namespace")),
+              Property(
+                  &HandleExpiredDocumentsResultProto::DocumentGroupInfo::schema,
+                  Eq("Person")),
+              Property(
+                  &HandleExpiredDocumentsResultProto::DocumentGroupInfo::uris,
+                  UnorderedElementsAre("person1", "person3"))),
+          AllOf(
+              Property(&HandleExpiredDocumentsResultProto::DocumentGroupInfo::
+                           name_space,
+                       Eq("namespace")),
+              Property(
+                  &HandleExpiredDocumentsResultProto::DocumentGroupInfo::schema,
+                  Eq("Email")),
+              Property(
+                  &HandleExpiredDocumentsResultProto::DocumentGroupInfo::uris,
+                  UnorderedElementsAre("email1", "email3")))));
+  EXPECT_THAT(result_proto.next_expiration_timestamp_ms(), Eq(1111));
+}
+
+TEST_F(
+    IcingSearchEngineDeleteTest,
+    HandleExpiredDocuments_taskSchedulerEnabled_shouldScheduleNextPurgingExpirationTask) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto person1 = DocumentBuilder()
+                              .SetKey("namespace", "person1")
+                              .SetSchema("Person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(5000)  // Expired at 5010 ms.
+                              .AddStringProperty("name", "Alice")
+                              .Build();
+  DocumentProto person2 = DocumentBuilder()
+                              .SetKey("namespace", "person2")
+                              .SetSchema("Person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(5400)  // Expired at 5410 ms.
+                              .AddStringProperty("name", "Bob")
+                              .Build();
+  DocumentProto email1 = DocumentBuilder()
+                             .SetKey("namespace", "email1")
+                             .SetSchema("Email")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("subject", "test")
+                             .AddStringProperty("sender", "namespace#person1")
+                             .Build();
+  DocumentProto email2 = DocumentBuilder()
+                             .SetKey("namespace", "email2")
+                             .SetSchema("Email")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("subject", "test")
+                             .AddStringProperty("sender", "namespace#person2")
+                             .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_background_task_scheduler(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(100);
+
+  {
+    // Initialize Icing and put all documents. Destruct Icing.
+    TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                                std::make_unique<IcingFilesystem>(),
+                                std::make_unique<FakeClock>(),
+                                GetTestJniCache());
+    InitializeResultProto initialize_result = icing.Initialize();
+    ASSERT_THAT(initialize_result.status(), ProtoIsOk());
+    EXPECT_THAT(
+        initialize_result.initialize_stats().next_expiration_timestamp_ms(),
+        Eq(-1));  // No next expiration timestamp since the database is empty.
+
+    ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(person1).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(person2).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(email1).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(email2).status(), ProtoIsOk());
+  }
+
+  // Initialize Icing again with a fake clock and t = 3500 ms. Initialization
+  // should schedule the purging expiration task at t = 5010 ms.
+  auto fake_clock = std::make_unique<FakeClock>();
+  FakeClock* fake_clock_ptr = fake_clock.get();
+  fake_clock->SetSystemTimeMilliseconds(3500);
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  InitializeResultProto initialize_result = icing.Initialize();
+  ASSERT_THAT(initialize_result.status(), ProtoIsOk());
+  EXPECT_THAT(
+      initialize_result.initialize_stats().next_expiration_timestamp_ms(),
+      Eq(5010));
+
+  // Sanity check that person1, person2, email1 and email2 are present.
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto1.mutable_document() = person1;
+  EXPECT_THAT(
+      icing.Get("namespace", "person1", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto1));
+
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_google::protobuf.mutable_document() = person2;
+  EXPECT_THAT(
+      icing.Get("namespace", "person2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_google::protobuf));
+
+  GetResultProto expected_get_result_proto3;
+  expected_get_result_proto3.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto3.mutable_document() = email1;
+  EXPECT_THAT(
+      icing.Get("namespace", "email1", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto3));
+
+  GetResultProto expected_get_result_proto4;
+  expected_get_result_proto4.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto4.mutable_document() = email2;
+  EXPECT_THAT(
+      icing.Get("namespace", "email2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto4));
+
+  // 1. Adjust the clock from 3500 to 5010 ms and sleep for 1550 ms. person1 and
+  //    email1 should be purged by the scheduled task, but person2 and email2
+  //    should still be alive.
+  fake_clock_ptr->SetSystemTimeMilliseconds(5010);
+  std::this_thread::sleep_for(std::chrono::milliseconds(1550));
+
+  GetResultProto expected_get_result_proto5;
+  expected_get_result_proto5.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto5.mutable_status()->set_message(
+      "Document (namespace, person1) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "person1", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto5));
+
+  GetResultProto expected_get_result_proto6;
+  expected_get_result_proto6.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto6.mutable_status()->set_message(
+      "Document (namespace, email1) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "email1", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto6));
+
+  EXPECT_THAT(
+      icing.Get("namespace", "person2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_google::protobuf));
+  EXPECT_THAT(
+      icing.Get("namespace", "email2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto4));
+
+  // 2. Adjust the clock from 5010 to 5410 ms and sleep for 400 ms. person2 and
+  //    email2 should be purged by the scheduled task.
+  fake_clock_ptr->SetSystemTimeMilliseconds(5410);
+  std::this_thread::sleep_for(std::chrono::milliseconds(400));
+
+  GetResultProto expected_get_result_proto7;
+  expected_get_result_proto7.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto7.mutable_status()->set_message(
+      "Document (namespace, person2) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "person2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto7));
+
+  GetResultProto expected_get_result_proto8;
+  expected_get_result_proto8.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto8.mutable_status()->set_message(
+      "Document (namespace, email2) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "email2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto8));
+}
+
 }  // namespace
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/icing-search-engine_fuzz_test.cc b/icing/icing-search-engine_fuzz_test.cc
index 6f1cf69..72ac01d 100644
--- a/icing/icing-search-engine_fuzz_test.cc
+++ b/icing/icing-search-engine_fuzz_test.cc
@@ -12,20 +12,23 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include <cstddef>
-#include <cstdint>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "gmock/gmock.h"
+#include "testing/fuzzing/fuzztest.h"
 #include "icing/document-builder.h"
+#include "icing/file/filesystem.h"
 #include "icing/icing-search-engine.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/initialize.pb.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/proto/scoring.pb.h"
 #include "icing/proto/search.pb.h"
+#include "icing/proto/status.pb.h"
 #include "icing/proto/term.pb.h"
 #include "icing/schema-builder.h"
+#include "icing/testing/common-matchers.h"
 #include "icing/testing/test-data.h"
 #include "icing/testing/tmp-directory.h"
 #include "icing/util/icu-data-file-helper.h"
@@ -40,32 +43,28 @@
   return icing_options;
 }
 
-DocumentProto MakeDocument(const uint8_t* data, size_t size) {
-  // TODO (sidchhabra): Added more optimized fuzzing techniques.
-  DocumentProto document;
-  std::string string_prop(reinterpret_cast<const char*>(data), size);
+DocumentProto MakeDocument(const std::string& data) {
   return DocumentBuilder()
       .SetKey("namespace", "uri1")
       .SetSchema("Message")
-      .AddStringProperty("body", string_prop)
+      .AddStringProperty("body", data)
+      .SetCreationTimestampMs(0L)
       .Build();
 }
 
-SearchSpecProto SetSearchSpec(const uint8_t* data, size_t size) {
+SearchSpecProto SetSearchSpec(const std::string& data) {
   SearchSpecProto search_spec;
-  search_spec.set_term_match_type(TermMatchType::PREFIX);
-  // TODO (sidchhabra): Added more optimized fuzzing techniques.
-  std::string query_string(reinterpret_cast<const char*>(data), size);
-  search_spec.set_query(query_string);
+  search_spec.set_term_match_type(TermMatchType::EXACT_ONLY);
+  search_spec.set_query(data);
   return search_spec;
 }
 
-extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+void StringFuzzTest(const std::string& data) {
   // Initialize
   IcingSearchEngineOptions icing_options = Setup();
   std::string icu_data_file_path = GetTestFilePath("icing/icu.dat");
   if (!icu_data_file_helper::SetUpIcuDataFile(icu_data_file_path).ok()) {
-    return 1;
+    return;
   }
   IcingSearchEngine icing(icing_options);
   const Filesystem filesystem_;
@@ -84,19 +83,36 @@
   icing.SetSchema(schema_proto);
 
   // Index
-  DocumentProto document = MakeDocument(data, size);
+  DocumentProto document = MakeDocument(data);
   icing.Put(document);
 
   // Query
-  SearchSpecProto search_spec = SetSearchSpec(data, size);
+  SearchSpecProto search_spec = SetSearchSpec(data);
   ScoringSpecProto scoring_spec;
   scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE);
-  ResultSpecProto result_spec;
-  libtextclassifier3::StatusOr<SearchResultProto> result =
-      icing.Search(search_spec, scoring_spec, result_spec);
-  return 0;
+  SearchResultProto result = icing.Search(search_spec, scoring_spec,
+                                          ResultSpecProto::default_instance());
+  EXPECT_THAT(result.results(0).document().uri(), document.uri());
 }
 
+// vector of emojis to be used in the fuzz test
+const std::vector<std::string> emojis = {"😀", "😂", "😊", "😘",
+  "😠", "😢", "😱", "🍻"};
+
+// add a random emoji to a string containing a decomposed character
+std::string addEmoji(const std::string& str, const std::string& emoji) {
+  return str + emoji;
+}
+
+// dictionary including Spanish words with accents (lowercase and uppercase)
+std::vector<std::string> data = {"dìas", "Sí", "Más", "Tú", "Él",
+  "Dónde", "Qué", "miércoles", "café",
+  "sábado"};
+
+FUZZ_TEST(TestSuite, StringFuzzTest)
+    .WithDomains(fuzztest::Map(addEmoji, fuzztest::ElementOf(data),
+                               fuzztest::ElementOf(emojis)));
+
 }  // namespace
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/icing-search-engine_initialization_test.cc b/icing/icing-search-engine_initialization_test.cc
index 5fc862b..836f849 100644
--- a/icing/icing-search-engine_initialization_test.cc
+++ b/icing/icing-search-engine_initialization_test.cc
@@ -13,11 +13,13 @@
 // limitations under the License.
 
 #include <algorithm>
+#include <chrono>  // NOLINT
 #include <cstdint>
 #include <limits>
 #include <memory>
 #include <string>
 #include <string_view>
+#include <thread>  // NOLINT
 #include <tuple>
 #include <unordered_set>
 #include <utility>
@@ -57,6 +59,7 @@
 #include "icing/legacy/index/icing-mock-filesystem.h"
 #include "icing/portable/endian.h"
 #include "icing/portable/equals-proto.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/debug.pb.h"
 #include "icing/proto/document.pb.h"
@@ -96,6 +99,7 @@
 #include "icing/transform/normalizer-options.h"
 #include "icing/transform/normalizer.h"
 #include "icing/util/clock.h"
+#include "icing/util/document-util.h"
 #include "icing/util/icu-data-file-helper.h"
 #include "icing/util/tokenized-document.h"
 #include "unicode/uloc.h"
@@ -260,8 +264,12 @@
   icing_options.set_enable_embedding_quantization(true);
   icing_options.set_enable_blob_store(true);
   icing_options.set_enable_qualified_id_join_index_v3(true);
+  icing_options.set_enable_soft_index_restoration(true);
   icing_options.set_enable_delete_propagation_from(false);
   icing_options.set_enable_marker_file_for_optimize(true);
+  icing_options.set_enable_proto_log_new_header_format(true);
+  icing_options.set_embedding_index_num_shards(32);
+  icing_options.set_enable_non_existent_qualified_id_join(true);
   return icing_options;
 }
 
@@ -500,6 +508,7 @@
        DeletePropagationEnabledAndJoinIndexV3DisabledReturnsInvalidArgument) {
   IcingSearchEngineOptions icing_options = GetDefaultIcingOptions();
   icing_options.set_enable_qualified_id_join_index_v3(false);
+  icing_options.set_enable_soft_index_restoration(true);
   icing_options.set_enable_delete_propagation_from(true);
 
   IcingSearchEngine icing(icing_options, GetTestJniCache());
@@ -508,7 +517,24 @@
               ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
   EXPECT_THAT(initialize_result_proto.status().message(),
               HasSubstr("Delete propagation is enabled but qualified id join "
-                        "index v3 is not enabled."));
+                        "index v3 or soft index restoration is not enabled."));
+}
+
+TEST_F(
+    IcingSearchEngineInitializationTest,
+    DeletePropagationEnabledAndSoftIndexRestorationDisabledReturnsInvalidArgument) {
+  IcingSearchEngineOptions icing_options = GetDefaultIcingOptions();
+  icing_options.set_enable_qualified_id_join_index_v3(true);
+  icing_options.set_enable_soft_index_restoration(false);
+  icing_options.set_enable_delete_propagation_from(true);
+
+  IcingSearchEngine icing(icing_options, GetTestJniCache());
+  InitializeResultProto initialize_result_proto = icing.Initialize();
+  EXPECT_THAT(initialize_result_proto.status(),
+              ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
+  EXPECT_THAT(initialize_result_proto.status().message(),
+              HasSubstr("Delete propagation is enabled but qualified id join "
+                        "index v3 or soft index restoration is not enabled."));
 }
 
 TEST_F(IcingSearchEngineInitializationTest, GoodCompressionLevelReturnsOk) {
@@ -543,6 +569,99 @@
   }
 }
 
+TEST_F(IcingSearchEngineInitializationTest,
+       OutOfRangeCompressionMemLevelReturnsInvalidArgument) {
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+
+  // Mem level must be between 1 and 9 inclusive.
+  options.set_compression_mem_level(-1);
+  {
+    IcingSearchEngine icing(options, GetTestJniCache());
+    EXPECT_THAT(icing.Initialize().status(),
+                ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
+  }
+
+  options.set_compression_mem_level(10);
+  {
+    IcingSearchEngine icing(options, GetTestJniCache());
+    EXPECT_THAT(icing.Initialize().status(),
+                ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
+  }
+
+  options.set_compression_mem_level(0);
+  {
+    IcingSearchEngine icing(options, GetTestJniCache());
+    EXPECT_THAT(icing.Initialize().status(),
+                ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
+  }
+}
+
+TEST_F(IcingSearchEngineInitializationTest,
+       OutOfRangeBlobStoreCompressionMemLevelReturnsInvalidArgument) {
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+
+  // Mem level must be between 1 and 9 inclusive.
+  options.set_blob_store_compression_mem_level(-1);
+  {
+    IcingSearchEngine icing(options, GetTestJniCache());
+    EXPECT_THAT(icing.Initialize().status(),
+                ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
+  }
+
+  options.set_blob_store_compression_mem_level(10);
+  {
+    IcingSearchEngine icing(options, GetTestJniCache());
+    EXPECT_THAT(icing.Initialize().status(),
+                ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
+  }
+
+  options.set_blob_store_compression_mem_level(0);
+  {
+    IcingSearchEngine icing(options, GetTestJniCache());
+    EXPECT_THAT(icing.Initialize().status(),
+                ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
+  }
+}
+
+TEST_F(IcingSearchEngineInitializationTest,
+       ReinitializingWithDifferentCompressionMemLevelOk) {
+  DocumentProto document =
+      DocumentBuilder()
+          .SetKey("icing", "fake_type/0")
+          .SetSchema("Message")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .AddStringProperty("body", "message body")
+          .AddInt64Property("indexableInteger", 123)
+          .Build();
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  {
+    // Initialize and put document
+    IcingSearchEngine icing(options, GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(document).status(), ProtoIsOk());
+    ASSERT_THAT(icing.PersistToDisk(PersistType::FULL).status(), ProtoIsOk());
+  }
+
+  // Reinitialize with different compression mem level and check that the
+  // document is still searchable.
+  options.set_compression_mem_level(1);
+  IcingSearchEngine icing(options, GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  search_spec.set_query("message");
+  search_spec.set_term_match_type(TermMatchType::EXACT_ONLY);
+  SearchResultProto expected_search_result_proto;
+  expected_search_result_proto.mutable_status()->set_code(StatusProto::OK);
+  *expected_search_result_proto.mutable_results()->Add()->mutable_document() =
+      document;
+  EXPECT_THAT(
+      icing.Search(search_spec, GetDefaultScoringSpec(),
+                   ResultSpecProto::default_instance()),
+      EqualsSearchResultIgnoreStatsAndScores(expected_search_result_proto));
+}
+
 TEST_F(IcingSearchEngineInitializationTest, FailToCreateDocStore) {
   auto mock_filesystem = std::make_unique<MockFilesystem>();
   // This fails DocumentStore::Create()
@@ -934,6 +1053,9 @@
   EXPECT_THAT(
       initialize_result.initialize_stats().embedding_index_restoration_cause(),
       Eq(InitializeStatsProto::NONE));
+  // needs_persist_type should be set to RECOVERY_PROOF.
+  EXPECT_THAT(initialize_result.needs_persist_type(),
+              Eq(PersistType::RECOVERY_PROOF));
 
   // ("namespace", "uri1") should be found.
   GetResultProto expected_get_result_proto;
@@ -1264,7 +1386,8 @@
                            /*lite_index_sort_size=*/1024 * 8);
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<Index> index,
-        Index::Create(options, filesystem(), icing_filesystem()));
+        Index::Create(options, filesystem(), icing_filesystem(),
+                      feature_flags_.get()));
     ICING_ASSERT_OK(index->Merge());
     ICING_ASSERT_OK(index->PersistToDisk());
   }
@@ -1294,14 +1417,18 @@
     std::string doc_store_dir = GetDocumentDir();
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(filesystem(), doc_store_dir, &fake_clock,
-                              schema_store.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            filesystem(), doc_store_dir, &fake_clock, schema_store.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     std::unique_ptr<DocumentStore> document_store =
         std::move(create_result.document_store);
 
@@ -1473,23 +1600,29 @@
             filesystem(), GetBlobDir(), &fake_clock,
             /*orphan_blob_time_to_live_ms=*/0,
             PortableFileBackedProtoLog<BlobInfoProto>::kDefaultCompressionLevel,
-            /*manage_blob_files=*/true));
+            protobuf_ports::kDefaultMemLevel,
+            /*manage_blob_files=*/true, feature_flags_.get()));
 
     // Puts message2 into DocumentStore but doesn't index it.
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(filesystem(), GetDocumentDir(), &fake_clock,
-                              schema_store.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            filesystem(), GetDocumentDir(), &fake_clock, schema_store.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     std::unique_ptr<DocumentStore> document_store =
         std::move(create_result.document_store);
 
-    ICING_EXPECT_OK(document_store->Put(message2));
+    ICING_EXPECT_OK(
+        document_store->Put(document_util::CreateDocumentWrapper(message2)));
   }
 
   // Mock filesystem to observe and check the behavior of all indices.
@@ -1789,6 +1922,9 @@
   EXPECT_THAT(initialize_result.initialize_stats()
                   .qualified_id_join_index_restoration_cause(),
               Eq(InitializeStatsProto::NONE));
+  // needs_persist_type should be set to RECOVERY_PROOF.
+  EXPECT_THAT(initialize_result.needs_persist_type(),
+              Eq(PersistType::RECOVERY_PROOF));
 
   // Verify join search: join a query for `name:person` with a child query for
   // `body:message` based on the child's `senderQualifiedId` field. message2
@@ -2485,7 +2621,7 @@
                                      /*index_merge_size=*/100,
                                      /*lite_index_sort_at_indexing=*/true,
                                      /*lite_index_sort_size=*/50),
-                      filesystem(), icing_filesystem()));
+                      filesystem(), icing_filesystem(), feature_flags_.get()));
     ICING_ASSERT_OK(index->PersistToDisk());
   }
 
@@ -3091,7 +3227,7 @@
                            /*index_merge_size=*/message.ByteSizeLong(),
                            /*lite_index_sort_at_indexing=*/true,
                            /*lite_index_sort_size=*/8),
-            filesystem(), icing_filesystem()));
+            filesystem(), icing_filesystem(), feature_flags_.get()));
     DocumentId original_last_added_doc_id = index->last_added_document_id();
     index->set_last_added_document_id(original_last_added_doc_id + 1);
     Index::Editor editor =
@@ -3223,7 +3359,7 @@
                            /*index_merge_size=*/message.ByteSizeLong(),
                            /*lite_index_sort_at_indexing=*/true,
                            /*lite_index_sort_size=*/8),
-            filesystem(), icing_filesystem()));
+            filesystem(), icing_filesystem(), feature_flags_.get()));
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<DocHitInfoIterator> doc_hit_info_iter,
         index->GetIterator("foo", /*term_start_index=*/0,
@@ -3337,7 +3473,7 @@
                            /*index_merge_size=*/message.ByteSizeLong(),
                            /*lite_index_sort_at_indexing=*/true,
                            /*lite_index_sort_size=*/8),
-            filesystem(), icing_filesystem()));
+            filesystem(), icing_filesystem(), feature_flags_.get()));
     DocumentId original_last_added_doc_id = index->last_added_document_id();
     index->set_last_added_document_id(original_last_added_doc_id + 1);
     Index::Editor editor =
@@ -3474,7 +3610,7 @@
                            /*index_merge_size=*/message.ByteSizeLong(),
                            /*lite_index_sort_at_indexing=*/true,
                            /*lite_index_sort_size=*/8),
-            filesystem(), icing_filesystem()));
+            filesystem(), icing_filesystem(), feature_flags_.get()));
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<DocHitInfoIterator> doc_hit_info_iter,
         index->GetIterator("foo", /*term_start_index=*/0,
@@ -3536,7 +3672,7 @@
             Index::Options(GetIndexDir(), /*index_merge_size=*/100,
                            /*lite_index_sort_at_indexing=*/true,
                            /*lite_index_sort_size=*/50),
-            filesystem(), icing_filesystem()));
+            filesystem(), icing_filesystem(), feature_flags_.get()));
     // Add hits for document 0 and merge.
     ASSERT_THAT(index->last_added_document_id(), kInvalidDocumentId);
     index->set_last_added_document_id(0);
@@ -3613,7 +3749,7 @@
         Index::Create(Index::Options(GetIndexDir(), /*index_merge_size=*/100,
                                      /*lite_index_sort_at_indexing=*/true,
                                      /*lite_index_sort_size=*/50),
-                      filesystem(), icing_filesystem()));
+                      filesystem(), icing_filesystem(), feature_flags_.get()));
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<DocHitInfoIterator> doc_hit_info_iter,
         index->GetIterator("foo", /*term_start_index=*/0,
@@ -3731,7 +3867,7 @@
                            /*index_merge_size=*/message.ByteSizeLong(),
                            /*lite_index_sort_at_indexing=*/true,
                            /*lite_index_sort_size=*/8),
-            filesystem(), icing_filesystem()));
+            filesystem(), icing_filesystem(), feature_flags_.get()));
     // Add hits for document 4 and merge.
     DocumentId original_last_added_doc_id = index->last_added_document_id();
     index->set_last_added_document_id(original_last_added_doc_id + 1);
@@ -3874,7 +4010,7 @@
         Index::Create(Index::Options(GetIndexDir(), /*index_merge_size=*/100,
                                      /*lite_index_sort_at_indexing=*/true,
                                      /*lite_index_sort_size=*/50),
-                      filesystem(), icing_filesystem()));
+                      filesystem(), icing_filesystem(), feature_flags_.get()));
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<DocHitInfoIterator> doc_hit_info_iter,
         index->GetIterator("foo", /*term_start_index=*/0,
@@ -4378,7 +4514,8 @@
         QualifiedIdJoinIndexImplV3::Create(
             filesystem, GetQualifiedIdJoinIndexDir(), *feature_flags_));
     EXPECT_THAT(qualified_id_join_index, Pointee(IsEmpty()));
-    EXPECT_THAT(qualified_id_join_index->Get(/*parent_document_id=*/0),
+    EXPECT_THAT(qualified_id_join_index->GetDocumentJoinIdPairArrayView(
+                    /*parent_document_id=*/0),
                 IsOkAndHolds(IsEmpty()));
   }
 }
@@ -4669,6 +4806,11 @@
     EXPECT_THAT(init_result.initialize_stats()
                     .qualified_id_join_index_restoration_cause(),
                 Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(
+        init_result.initialize_stats().embedding_index_restoration_cause(),
+        Eq(InitializeStatsProto::NONE));
+    // PersistToDisk is not needed.
+    EXPECT_THAT(init_result.needs_persist_type(), Eq(PersistType::UNKNOWN));
   }
 }
 
@@ -4735,10 +4877,505 @@
     EXPECT_THAT(init_result.initialize_stats()
                     .qualified_id_join_index_restoration_cause(),
                 Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(
+        init_result.initialize_stats().embedding_index_restoration_cause(),
+        Eq(InitializeStatsProto::NONE));
+    // PersistToDisk is not needed.
+    EXPECT_THAT(init_result.needs_persist_type(), Eq(PersistType::UNKNOWN));
   }
 }
 
 TEST_F(IcingSearchEngineInitializationTest,
+       InitializeShouldPurgeDocumentsThatExpiredDuringOffline) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Message")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("body")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_REQUIRED))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+
+  DocumentProto person = DocumentBuilder()
+                             .SetKey("namespace", "person")
+                             .SetSchema("Person")
+                             .AddStringProperty("name", "person")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(10000)  // Expire at 10010 ms.
+                             .Build();
+  DocumentProto message = DocumentBuilder()
+                              .SetKey("namespace", "message/1")
+                              .SetSchema("Message")
+                              .AddStringProperty("body", "message body")
+                              .AddStringProperty("sender", "namespace#person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(0)  // Never expire.
+                              .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  {
+    // Initialize Icing and put person and message documents. Destruct Icing.
+    TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                                std::make_unique<IcingFilesystem>(),
+                                std::make_unique<FakeClock>(),
+                                GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(person).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(message).status(), ProtoIsOk());
+  }
+
+  // Initialize Icing again with a fake clock and t = 20000 ms. Initialization
+  // should purge the person document (since it expired at t = 10010 ms) and
+  // apply delete propagation to the message document.
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetSystemTimeMilliseconds(20000);
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  InitializeResultProto initialize_result = icing.Initialize();
+  EXPECT_THAT(initialize_result.status(), ProtoIsOk());
+
+  // Verify that person and message documents are purged.
+  // - At t = 20000 ms, the person document is expired, but DocumentFilterData
+  //   has already filtered it out, so we're unsure whether it is purged or not.
+  // - But the message document never expires and DocumentFilterData doesn't
+  //   filter it out. If Initialize() had not handled expired documents
+  //   correctly, then expiration propagation would have not been triggered and
+  //   the message document would've been alive. Therefore, we can verify it by
+  //   checking the message document.
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto1.mutable_status()->set_message(
+      "Document (namespace, person) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "person", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto1));
+
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_google::protobuf.mutable_status()->set_message(
+      "Document (namespace, message/1) not found.");
+  EXPECT_THAT(icing.Get("namespace", "message/1",
+                        GetResultSpecProto::default_instance()),
+              EqualsProto(expected_get_result_google::protobuf));
+}
+
+TEST_F(
+    IcingSearchEngineInitializationTest,
+    Initialize_taskSchedulerEnabled_shouldSchedulePurgingExpiredDocumentsTask) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Message")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("body")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_REQUIRED))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+
+  DocumentProto person1 = DocumentBuilder()
+                              .SetKey("namespace", "person1")
+                              .SetSchema("Person")
+                              .AddStringProperty("name", "person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(10000)  // Expire at 10010 ms.
+                              .Build();
+  DocumentProto person2 = DocumentBuilder()
+                              .SetKey("namespace", "person2")
+                              .SetSchema("Person")
+                              .AddStringProperty("name", "person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(21000)  // Expire at 21010 ms.
+                              .Build();
+  DocumentProto message1 = DocumentBuilder()
+                               .SetKey("namespace", "message/1")
+                               .SetSchema("Message")
+                               .AddStringProperty("body", "message body")
+                               .AddStringProperty("sender", "namespace#person1")
+                               .SetCreationTimestampMs(10)
+                               .SetTtlMs(0)  // Never expire.
+                               .Build();
+  DocumentProto message2 = DocumentBuilder()
+                               .SetKey("namespace", "message/2")
+                               .SetSchema("Message")
+                               .AddStringProperty("body", "message body")
+                               .AddStringProperty("sender", "namespace#person2")
+                               .SetCreationTimestampMs(10)
+                               .SetTtlMs(0)  // Never expire.
+                               .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_background_task_scheduler(true);
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  {
+    // Initialize Icing and put all documents. Destruct Icing.
+    TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                                std::make_unique<IcingFilesystem>(),
+                                std::make_unique<FakeClock>(),
+                                GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(person1).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(person2).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(message1).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(message2).status(), ProtoIsOk());
+  }
+
+  // Initialize Icing again with a fake clock and t = 20000 ms. Initialization
+  // should purge person1 (since it expired at t = 10010 ms) and apply delete
+  // propagation to message1. Also it should schedule the puring expired
+  // documents task for the next expiration event.
+  auto fake_clock = std::make_unique<FakeClock>();
+  FakeClock* fake_clock_ptr = fake_clock.get();
+  fake_clock->SetSystemTimeMilliseconds(20000);
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  InitializeResultProto initialize_result = icing.Initialize();
+  EXPECT_THAT(initialize_result.status(), ProtoIsOk());
+  // Next expiration timestamp should be set correctly.
+  EXPECT_THAT(
+      initialize_result.initialize_stats().next_expiration_timestamp_ms(),
+      Eq(21010));
+
+  // Sanity check: person2 and message2 are still alive.
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto1.mutable_document() = person2;
+  EXPECT_THAT(
+      icing.Get("namespace", "person2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto1));
+
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_google::protobuf.mutable_document() = message2;
+  EXPECT_THAT(icing.Get("namespace", "message/2",
+                        GetResultSpecProto::default_instance()),
+              EqualsProto(expected_get_result_google::protobuf));
+
+  // Adjust the clock to 21010 ms and sleep for 1010 ms. The purging expiration
+  // task should execute and purge person2 and message2.
+  // - At t = 21010 ms, person2 is expired, but DocumentFilterData has already
+  //   filtered it out, so we're unsure whether it is purged or not.
+  // - But message2 never expires and DocumentFilterData doesn't filter it out.
+  //   If Initialize() had not scheduled the purging expired documents task
+  //   correctly for the next expiration event (i.e. for person2), then
+  //   expiration propagation would have not been triggered and message2
+  //   would've been alive. Therefore, we can verify it by checking message2
+  fake_clock_ptr->SetSystemTimeMilliseconds(21010);
+  std::this_thread::sleep_for(std::chrono::milliseconds(1010));
+
+  GetResultProto expected_get_result_proto3;
+  expected_get_result_proto3.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto3.mutable_status()->set_message(
+      "Document (namespace, person2) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "person2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto3));
+
+  GetResultProto expected_get_result_proto4;
+  expected_get_result_proto4.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto4.mutable_status()->set_message(
+      "Document (namespace, message/2) not found.");
+  EXPECT_THAT(icing.Get("namespace", "message/2",
+                        GetResultSpecProto::default_instance()),
+              EqualsProto(expected_get_result_proto4));
+}
+
+TEST_F(
+    IcingSearchEngineInitializationTest,
+    Initialize_taskSchedulerDisabled_shouldPurgeExpiredDocumentsButNoRescheduling) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Message")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("body")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_REQUIRED))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+
+  DocumentProto person1 = DocumentBuilder()
+                              .SetKey("namespace", "person1")
+                              .SetSchema("Person")
+                              .AddStringProperty("name", "person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(10000)  // Expire at 10010 ms.
+                              .Build();
+  DocumentProto person2 = DocumentBuilder()
+                              .SetKey("namespace", "person2")
+                              .SetSchema("Person")
+                              .AddStringProperty("name", "person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(21000)  // Expire at 21010 ms.
+                              .Build();
+  DocumentProto message1 = DocumentBuilder()
+                               .SetKey("namespace", "message/1")
+                               .SetSchema("Message")
+                               .AddStringProperty("body", "message body")
+                               .AddStringProperty("sender", "namespace#person1")
+                               .SetCreationTimestampMs(10)
+                               .SetTtlMs(0)  // Never expire.
+                               .Build();
+  DocumentProto message2 = DocumentBuilder()
+                               .SetKey("namespace", "message/2")
+                               .SetSchema("Message")
+                               .AddStringProperty("body", "message body")
+                               .AddStringProperty("sender", "namespace#person2")
+                               .SetCreationTimestampMs(10)
+                               .SetTtlMs(0)  // Never expire.
+                               .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_background_task_scheduler(false);
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  {
+    // Initialize Icing and put all documents. Destruct Icing.
+    TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                                std::make_unique<IcingFilesystem>(),
+                                std::make_unique<FakeClock>(),
+                                GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(person1).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(person2).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(message1).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(message2).status(), ProtoIsOk());
+  }
+
+  // Initialize Icing again with a fake clock and t = 20000 ms.
+  // - Initialization should purge person1 (since it expired at t = 10010 ms)
+  //   and apply delete propagation to message1.
+  // - Since the task scheduler is disabled, it should NOT schedule the puring
+  //   expired documents task for the next expiration event.
+  auto fake_clock = std::make_unique<FakeClock>();
+  FakeClock* fake_clock_ptr = fake_clock.get();
+  fake_clock->SetSystemTimeMilliseconds(20000);
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  InitializeResultProto initialize_result = icing.Initialize();
+  EXPECT_THAT(initialize_result.status(), ProtoIsOk());
+  // Next expiration timestamp should be set correctly.
+  EXPECT_THAT(
+      initialize_result.initialize_stats().next_expiration_timestamp_ms(),
+      Eq(21010));
+
+  // Sanity check: person2 and message2 are still alive.
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto1.mutable_document() = person2;
+  EXPECT_THAT(
+      icing.Get("namespace", "person2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto1));
+
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_google::protobuf.mutable_document() = message2;
+  EXPECT_THAT(icing.Get("namespace", "message/2",
+                        GetResultSpecProto::default_instance()),
+              EqualsProto(expected_get_result_google::protobuf));
+
+  // Adjust the clock to 21010 ms and sleep for 1010 ms. message2 should still
+  // be present.
+  fake_clock_ptr->SetSystemTimeMilliseconds(21010);
+  std::this_thread::sleep_for(std::chrono::milliseconds(1010));
+  EXPECT_THAT(icing.Get("namespace", "message/2",
+                        GetResultSpecProto::default_instance()),
+              EqualsProto(expected_get_result_google::protobuf));
+}
+
+TEST_F(IcingSearchEngineInitializationTest,
+       IndexRecovery_failingDocumentShouldPropagateDeletionToChildDocuments) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Person")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("name")
+                                        .SetDataTypeString(TERM_MATCH_PREFIX,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("timestamp")
+                                        .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                        .SetCardinality(CARDINALITY_REQUIRED)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Message")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("body")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_REQUIRED))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+
+  DocumentProto person = DocumentBuilder()
+                             .SetKey("namespace", "person1")
+                             .SetSchema("Person")
+                             .AddStringProperty("name", "person")
+                             .AddInt64Property("timestamp", 1024)
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(10000)  // Expire at 10010 ms.
+                             .Build();
+  DocumentProto message = DocumentBuilder()
+                              .SetKey("namespace", "message/1")
+                              .SetSchema("Message")
+                              .AddStringProperty("body", "message body")
+                              .AddStringProperty("sender", "namespace#person1")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(0)  // Never expire.
+                              .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  {
+    // Initialize Icing and put all documents. Destruct Icing.
+    TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                                std::make_unique<IcingFilesystem>(),
+                                std::make_unique<FakeClock>(),
+                                GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(person).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(message).status(), ProtoIsOk());
+  }
+
+  // 2. Delete integer index to trigger index restoration.
+  ASSERT_TRUE(
+      filesystem()->DeleteDirectoryRecursively(GetIntegerIndexDir().c_str()));
+
+  // 3. Mock filesystem to fail creating "timestamp" integer index storage.
+  auto mock_filesystem = std::make_unique<MockFilesystem>();
+  ON_CALL(*mock_filesystem,
+          CreateDirectory(HasSubstr(GetIntegerIndexDir() + "/timestamp")))
+      .WillByDefault(Return(false));
+
+  // 4. Initialize IcingSearchEngine again with the mock filesystem. When
+  //    indexing document "person1", it will fail to create "timestamp" integer
+  //    index storage, but soft index restoration mechanism should skip the
+  //    error and delete the document without failing initialization.
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetTimerElapsedMilliseconds(10);
+  TestIcingSearchEngine icing(options, std::move(mock_filesystem),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+
+  InitializeResultProto initialize_result = icing.Initialize();
+  EXPECT_THAT(initialize_result.status(),
+              ProtoStatusIs(StatusProto::WARNING_DATA_LOSS));
+
+  EXPECT_THAT(
+      initialize_result.initialize_stats().document_store_recovery_cause(),
+      Eq(InitializeStatsProto::NONE));
+  // Indices should be restored. "person1" should fail to be reindexed.
+  EXPECT_THAT(
+      initialize_result.initialize_stats().index_restoration_latency_ms(),
+      Eq(10));
+  EXPECT_THAT(
+      initialize_result.initialize_stats().num_failed_reindexed_documents(),
+      Eq(1));
+  EXPECT_THAT(initialize_result.initialize_stats().index_restoration_cause(),
+              Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(
+      initialize_result.initialize_stats().integer_index_restoration_cause(),
+      Eq(InitializeStatsProto::INCONSISTENT_WITH_GROUND_TRUTH));
+  EXPECT_THAT(initialize_result.initialize_stats()
+                  .qualified_id_join_index_restoration_cause(),
+              Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(
+      initialize_result.initialize_stats().embedding_index_restoration_cause(),
+      Eq(InitializeStatsProto::NONE));
+  // needs_persist_type should be set to RECOVERY_PROOF.
+  EXPECT_THAT(initialize_result.needs_persist_type(),
+              Eq(PersistType::RECOVERY_PROOF));
+
+  // ("namespace", "person1") should be deleted.
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto1.mutable_status()->set_message(
+      "Document (namespace, person1) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "person1", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto1));
+
+  // ("namespace", "message/1") should be deleted. Although this document should
+  // successfully be reindexed, when "person1" fails to be reindexed, all
+  // of its child documents should be deleted.
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_google::protobuf.mutable_status()->set_message(
+      "Document (namespace, message/1) not found.");
+  EXPECT_THAT(icing.Get("namespace", "message/1",
+                        GetResultSpecProto::default_instance()),
+              EqualsProto(expected_get_result_google::protobuf));
+}
+
+TEST_F(IcingSearchEngineInitializationTest,
        InitializeShouldLogFunctionLatency) {
   auto fake_clock = std::make_unique<FakeClock>();
   fake_clock->SetTimerElapsedMilliseconds(10);
@@ -4810,6 +5447,12 @@
                               std::move(fake_clock), GetTestJniCache());
   InitializeResultProto initialize_result_proto = icing.Initialize();
   EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk());
+  EXPECT_THAT(
+      initialize_result_proto.initialize_stats().schema_store_recovery_cause(),
+      Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result_proto.initialize_stats()
+                  .schema_store_recovery_latency_ms(),
+              Eq(0));
   EXPECT_THAT(initialize_result_proto.initialize_stats()
                   .document_store_recovery_cause(),
               Eq(InitializeStatsProto::NONE));
@@ -4828,15 +5471,59 @@
   EXPECT_THAT(initialize_result_proto.initialize_stats()
                   .qualified_id_join_index_restoration_cause(),
               Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result_proto.initialize_stats()
+                  .embedding_index_restoration_cause(),
+              Eq(InitializeStatsProto::NONE));
   EXPECT_THAT(
       initialize_result_proto.initialize_stats().index_restoration_latency_ms(),
       Eq(0));
+  // PersistToDisk is not needed.
+  EXPECT_THAT(initialize_result_proto.needs_persist_type(),
+              Eq(PersistType::UNKNOWN));
+
+  // Initialize another Icing instance with the "existing empty database". Even
+  // though we never call PersistToDisk(), there should be no recovery.
+  auto fake_clock2 = std::make_unique<FakeClock>();
+  fake_clock2->SetTimerElapsedMilliseconds(10);
+  TestIcingSearchEngine another_icing(
+      GetDefaultIcingOptions(), std::make_unique<Filesystem>(),
+      std::make_unique<IcingFilesystem>(), std::move(fake_clock2),
+      GetTestJniCache());
+  InitializeResultProto initialize_result_google::protobuf = another_icing.Initialize();
+  EXPECT_THAT(initialize_result_google::protobuf.status(), ProtoIsOk());
   EXPECT_THAT(
-      initialize_result_proto.initialize_stats().schema_store_recovery_cause(),
+      initialize_result_google::protobuf.initialize_stats().schema_store_recovery_cause(),
       Eq(InitializeStatsProto::NONE));
-  EXPECT_THAT(initialize_result_proto.initialize_stats()
+  EXPECT_THAT(initialize_result_google::protobuf.initialize_stats()
                   .schema_store_recovery_latency_ms(),
               Eq(0));
+  EXPECT_THAT(initialize_result_google::protobuf.initialize_stats()
+                  .document_store_recovery_cause(),
+              Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result_google::protobuf.initialize_stats()
+                  .document_store_recovery_latency_ms(),
+              Eq(0));
+  EXPECT_THAT(
+      initialize_result_google::protobuf.initialize_stats().document_store_data_status(),
+      Eq(InitializeStatsProto::NO_DATA_LOSS));
+  EXPECT_THAT(
+      initialize_result_google::protobuf.initialize_stats().index_restoration_cause(),
+      Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result_google::protobuf.initialize_stats()
+                  .integer_index_restoration_cause(),
+              Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result_google::protobuf.initialize_stats()
+                  .qualified_id_join_index_restoration_cause(),
+              Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result_google::protobuf.initialize_stats()
+                  .embedding_index_restoration_cause(),
+              Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result_google::protobuf.initialize_stats()
+                  .index_restoration_latency_ms(),
+              Eq(0));
+  // PersistToDisk is not needed.
+  EXPECT_THAT(initialize_result_google::protobuf.needs_persist_type(),
+              Eq(PersistType::UNKNOWN));
 }
 
 TEST_F(IcingSearchEngineInitializationTest,
@@ -4903,6 +5590,9 @@
                     .qualified_id_join_index_restoration_cause(),
                 Eq(InitializeStatsProto::NONE));
     EXPECT_THAT(initialize_result_proto.initialize_stats()
+                    .embedding_index_restoration_cause(),
+                Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .index_restoration_latency_ms(),
                 Eq(0));
     EXPECT_THAT(initialize_result_proto.initialize_stats()
@@ -4911,6 +5601,10 @@
     EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .schema_store_recovery_latency_ms(),
                 Eq(0));
+
+    // needs_persist_type should be set to RECOVERY_PROOF.
+    EXPECT_THAT(initialize_result_proto.needs_persist_type(),
+                Eq(PersistType::RECOVERY_PROOF));
   }
 }
 
@@ -4960,7 +5654,7 @@
 
     // Set dirty bit to true to reflect that something changed in the log.
     header.SetDirtyFlag(true);
-    header.SetHeaderChecksum(header.CalculateHeaderChecksum());
+    header.UpdateHeaderChecksums(/*enable_new_header_format=*/true);
 
     WriteDocumentLogHeader(*filesystem(), document_log_file, header);
   }
@@ -4997,6 +5691,9 @@
                     .qualified_id_join_index_restoration_cause(),
                 Eq(InitializeStatsProto::NONE));
     EXPECT_THAT(initialize_result_proto.initialize_stats()
+                    .embedding_index_restoration_cause(),
+                Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .index_restoration_latency_ms(),
                 Eq(0));
     EXPECT_THAT(initialize_result_proto.initialize_stats()
@@ -5005,6 +5702,10 @@
     EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .schema_store_recovery_latency_ms(),
                 Eq(0));
+
+    // needs_persist_type should be set to RECOVERY_PROOF.
+    EXPECT_THAT(initialize_result_proto.needs_persist_type(),
+                Eq(PersistType::RECOVERY_PROOF));
   }
 }
 
@@ -5035,7 +5736,7 @@
                                      /*index_merge_size=*/100,
                                      /*lite_index_sort_at_indexing=*/true,
                                      /*lite_index_sort_size=*/50),
-                      filesystem(), icing_filesystem()));
+                      filesystem(), icing_filesystem(), feature_flags_.get()));
     ICING_ASSERT_OK(index->PersistToDisk());
   }
 
@@ -5060,6 +5761,9 @@
                     .qualified_id_join_index_restoration_cause(),
                 Eq(InitializeStatsProto::NONE));
     EXPECT_THAT(initialize_result_proto.initialize_stats()
+                    .embedding_index_restoration_cause(),
+                Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .index_restoration_latency_ms(),
                 Eq(10));
     EXPECT_THAT(initialize_result_proto.initialize_stats()
@@ -5077,6 +5781,10 @@
     EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .schema_store_recovery_latency_ms(),
                 Eq(0));
+
+    // needs_persist_type should be set to RECOVERY_PROOF.
+    EXPECT_THAT(initialize_result_proto.needs_persist_type(),
+                Eq(PersistType::RECOVERY_PROOF));
   }
 }
 
@@ -5124,6 +5832,9 @@
                     .qualified_id_join_index_restoration_cause(),
                 Eq(InitializeStatsProto::NONE));
     EXPECT_THAT(initialize_result_proto.initialize_stats()
+                    .embedding_index_restoration_cause(),
+                Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .index_restoration_latency_ms(),
                 Eq(10));
     EXPECT_THAT(initialize_result_proto.initialize_stats()
@@ -5141,6 +5852,10 @@
     EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .schema_store_recovery_latency_ms(),
                 Eq(0));
+
+    // needs_persist_type should be set to RECOVERY_PROOF.
+    EXPECT_THAT(initialize_result_proto.needs_persist_type(),
+                Eq(PersistType::RECOVERY_PROOF));
   }
 }
 
@@ -5226,6 +5941,9 @@
                     .qualified_id_join_index_restoration_cause(),
                 Eq(InitializeStatsProto::INCONSISTENT_WITH_GROUND_TRUTH));
     EXPECT_THAT(initialize_result_proto.initialize_stats()
+                    .embedding_index_restoration_cause(),
+                Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .index_restoration_latency_ms(),
                 Eq(10));
     EXPECT_THAT(initialize_result_proto.initialize_stats()
@@ -5243,6 +5961,10 @@
     EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .schema_store_recovery_latency_ms(),
                 Eq(0));
+
+    // needs_persist_type should be set to RECOVERY_PROOF.
+    EXPECT_THAT(initialize_result_proto.needs_persist_type(),
+                Eq(PersistType::RECOVERY_PROOF));
   }
 }
 
@@ -5296,7 +6018,7 @@
   }
 
   {
-    // Both document store and index should be recovered from checksum mismatch.
+    // All derived files should be rebuilt.
     auto fake_clock = std::make_unique<FakeClock>();
     fake_clock->SetTimerElapsedMilliseconds(10);
     TestIcingSearchEngine icing(GetDefaultIcingOptions(),
@@ -5347,10 +6069,14 @@
     EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .embedding_index_restoration_cause(),
                 Eq(InitializeStatsProto::SCHEMA_CHANGES_OUT_OF_SYNC));
+
+    // needs_persist_type should be set to RECOVERY_PROOF.
+    EXPECT_THAT(initialize_result_proto.needs_persist_type(),
+                Eq(PersistType::RECOVERY_PROOF));
   }
 
   {
-    // No recovery should be needed.
+    // Initialize again. No recovery should be needed.
     auto fake_clock = std::make_unique<FakeClock>();
     fake_clock->SetTimerElapsedMilliseconds(10);
     TestIcingSearchEngine icing(GetDefaultIcingOptions(),
@@ -5401,6 +6127,10 @@
     EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .embedding_index_restoration_cause(),
                 Eq(InitializeStatsProto::NONE));
+
+    // PersistToDisk is not needed.
+    EXPECT_THAT(initialize_result_proto.needs_persist_type(),
+                Eq(PersistType::UNKNOWN));
   }
 }
 
@@ -5443,14 +6173,18 @@
     std::string doc_store_dir = GetDocumentDir();
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(filesystem(), doc_store_dir, fake_clock.get(),
-                              schema_store.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            filesystem(), doc_store_dir, fake_clock.get(), schema_store.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     std::unique_ptr<DocumentStore> document_store =
         std::move(create_result.document_store);
 
@@ -5529,6 +6263,10 @@
     EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .embedding_index_restoration_cause(),
                 Eq(InitializeStatsProto::OPTIMIZE_OUT_OF_SYNC));
+
+    // needs_persist_type should be set to RECOVERY_PROOF.
+    EXPECT_THAT(initialize_result_proto.needs_persist_type(),
+                Eq(PersistType::RECOVERY_PROOF));
   }
 
   {
@@ -5583,6 +6321,10 @@
     EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .embedding_index_restoration_cause(),
                 Eq(InitializeStatsProto::NONE));
+
+    // PersistToDisk is not needed.
+    EXPECT_THAT(initialize_result_proto.needs_persist_type(),
+                Eq(PersistType::UNKNOWN));
   }
 }
 
@@ -5631,6 +6373,9 @@
   EXPECT_THAT(initialize_result_proto.initialize_stats()
                   .qualified_id_join_index_restoration_cause(),
               Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result_proto.initialize_stats()
+                  .embedding_index_restoration_cause(),
+              Eq(InitializeStatsProto::NONE));
   EXPECT_THAT(
       initialize_result_proto.initialize_stats().index_restoration_latency_ms(),
       Eq(10));
@@ -5649,6 +6394,10 @@
   EXPECT_THAT(initialize_result_proto.initialize_stats()
                   .schema_store_recovery_latency_ms(),
               Eq(0));
+
+  // needs_persist_type should be set to RECOVERY_PROOF.
+  EXPECT_THAT(initialize_result_proto.needs_persist_type(),
+              Eq(PersistType::RECOVERY_PROOF));
 }
 
 TEST_F(IcingSearchEngineInitializationTest,
@@ -5694,6 +6443,9 @@
   EXPECT_THAT(initialize_result_proto.initialize_stats()
                   .qualified_id_join_index_restoration_cause(),
               Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result_proto.initialize_stats()
+                  .embedding_index_restoration_cause(),
+              Eq(InitializeStatsProto::NONE));
   EXPECT_THAT(
       initialize_result_proto.initialize_stats().index_restoration_latency_ms(),
       Eq(10));
@@ -5712,6 +6464,10 @@
   EXPECT_THAT(initialize_result_proto.initialize_stats()
                   .schema_store_recovery_latency_ms(),
               Eq(0));
+
+  // needs_persist_type should be set to RECOVERY_PROOF.
+  EXPECT_THAT(initialize_result_proto.needs_persist_type(),
+              Eq(PersistType::RECOVERY_PROOF));
 }
 
 TEST_F(IcingSearchEngineInitializationTest,
@@ -5797,6 +6553,9 @@
   EXPECT_THAT(initialize_result_proto.initialize_stats()
                   .qualified_id_join_index_restoration_cause(),
               Eq(InitializeStatsProto::IO_ERROR));
+  EXPECT_THAT(initialize_result_proto.initialize_stats()
+                  .embedding_index_restoration_cause(),
+              Eq(InitializeStatsProto::NONE));
   EXPECT_THAT(
       initialize_result_proto.initialize_stats().index_restoration_latency_ms(),
       Eq(10));
@@ -5815,6 +6574,103 @@
   EXPECT_THAT(initialize_result_proto.initialize_stats()
                   .schema_store_recovery_latency_ms(),
               Eq(0));
+
+  // needs_persist_type should be set to RECOVERY_PROOF.
+  EXPECT_THAT(initialize_result_proto.needs_persist_type(),
+              Eq(PersistType::RECOVERY_PROOF));
+}
+
+TEST_F(IcingSearchEngineInitializationTest,
+       InitializeShouldLogRecoveryCauseEmbeddingIndexIOError) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("subject")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_REPEATED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("embedding")
+                                        .SetDataTypeVector(
+                                            EMBEDDING_INDEXING_LINEAR_SEARCH)
+                                        .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+
+  DocumentProto email =
+      DocumentBuilder()
+          .SetKey("namespace", "email")
+          .SetSchema("Email")
+          .AddStringProperty("subject", "test subject")
+          .AddVectorProperty(
+              "embedding",
+              CreateVector("my_model_v1", {0.1, 0.2, 0.3, 0.4, 0.5}),
+              CreateVector("my_model_v1", {1, 2, 3, 4, 5}),
+              CreateVector("my_model_v1", {0.6, 0.7, 0.8, 0.9, -1}))
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+
+  {
+    // Initialize and put documents.
+    IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(email).status(), ProtoIsOk());
+  }
+
+  std::string embedding_index_metadata_file =
+      absl_ports::StrCat(GetEmbeddingIndexDir(), "/metadata");
+  auto mock_filesystem = std::make_unique<MockFilesystem>();
+  EXPECT_CALL(*mock_filesystem, OpenForWrite(_)).WillRepeatedly(DoDefault());
+  // This fails EmbeddingIndex::Create() once.
+  EXPECT_CALL(*mock_filesystem, OpenForWrite(Eq(embedding_index_metadata_file)))
+      .WillOnce(Return(-1))
+      .WillRepeatedly(DoDefault());
+
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetTimerElapsedMilliseconds(10);
+  TestIcingSearchEngine icing(GetDefaultIcingOptions(),
+                              std::move(mock_filesystem),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+
+  InitializeResultProto initialize_result_proto = icing.Initialize();
+  EXPECT_THAT(initialize_result_proto.status(), ProtoIsOk());
+  EXPECT_THAT(
+      initialize_result_proto.initialize_stats().index_restoration_cause(),
+      Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result_proto.initialize_stats()
+                  .integer_index_restoration_cause(),
+              Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result_proto.initialize_stats()
+                  .qualified_id_join_index_restoration_cause(),
+              Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result_proto.initialize_stats()
+                  .embedding_index_restoration_cause(),
+              Eq(InitializeStatsProto::IO_ERROR));
+  EXPECT_THAT(
+      initialize_result_proto.initialize_stats().index_restoration_latency_ms(),
+      Eq(10));
+  EXPECT_THAT(initialize_result_proto.initialize_stats()
+                  .document_store_recovery_cause(),
+              Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result_proto.initialize_stats()
+                  .document_store_recovery_latency_ms(),
+              Eq(0));
+  EXPECT_THAT(
+      initialize_result_proto.initialize_stats().document_store_data_status(),
+      Eq(InitializeStatsProto::NO_DATA_LOSS));
+  EXPECT_THAT(
+      initialize_result_proto.initialize_stats().schema_store_recovery_cause(),
+      Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result_proto.initialize_stats()
+                  .schema_store_recovery_latency_ms(),
+              Eq(0));
+
+  // needs_persist_type should be set to RECOVERY_PROOF.
+  EXPECT_THAT(initialize_result_proto.needs_persist_type(),
+              Eq(PersistType::RECOVERY_PROOF));
 }
 
 TEST_F(IcingSearchEngineInitializationTest,
@@ -5872,6 +6728,9 @@
   EXPECT_THAT(initialize_result_proto.initialize_stats()
                   .qualified_id_join_index_restoration_cause(),
               Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result_proto.initialize_stats()
+                  .embedding_index_restoration_cause(),
+              Eq(InitializeStatsProto::NONE));
   EXPECT_THAT(
       initialize_result_proto.initialize_stats().index_restoration_latency_ms(),
       Eq(0));
@@ -5881,6 +6740,10 @@
   EXPECT_THAT(initialize_result_proto.initialize_stats()
                   .schema_store_recovery_latency_ms(),
               Eq(0));
+
+  // needs_persist_type should be set to RECOVERY_PROOF.
+  EXPECT_THAT(initialize_result_proto.needs_persist_type(),
+              Eq(PersistType::RECOVERY_PROOF));
 }
 
 TEST_F(IcingSearchEngineInitializationTest,
@@ -5933,8 +6796,15 @@
                     .qualified_id_join_index_restoration_cause(),
                 Eq(InitializeStatsProto::NONE));
     EXPECT_THAT(initialize_result_proto.initialize_stats()
+                    .embedding_index_restoration_cause(),
+                Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .index_restoration_latency_ms(),
                 Eq(0));
+
+    // needs_persist_type should be set to RECOVERY_PROOF.
+    EXPECT_THAT(initialize_result_proto.needs_persist_type(),
+                Eq(PersistType::RECOVERY_PROOF));
   }
 }
 
@@ -5977,6 +6847,379 @@
   }
 }
 
+TEST_F(IcingSearchEngineInitializationTest,
+       ProtoLogNewHeaderFormat_switchFlagFromOnToOffShouldDiscardUnsyncedTail) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+
+  DocumentProto document0 =
+      DocumentBuilder()
+          .SetKey("namespace", "uri/0")
+          .SetSchema("Person")
+          .AddStringProperty("name", "person")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto document1 =
+      DocumentBuilder()
+          .SetKey("namespace", "uri/1")
+          .SetSchema("Person")
+          .AddStringProperty("name", "person")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_proto_log_new_header_format(true);
+  IcingSearchEngine icing(options, GetTestJniCache());
+
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+
+  // Put document0 and persist to disk.
+  ASSERT_THAT(icing.Put(document0).status(), ProtoIsOk());
+  ASSERT_THAT(icing.PersistToDisk(PersistType::FULL).status(), ProtoIsOk());
+
+  // Put document1 and don't persist to disk.
+  ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk());
+
+  // Initialize another Icing instance with the flag off.
+  IcingSearchEngineOptions another_options = GetDefaultIcingOptions();
+  another_options.set_enable_proto_log_new_header_format(false);
+  IcingSearchEngine another_icing(another_options, GetTestJniCache());
+
+  InitializeResultProto initialize_result = another_icing.Initialize();
+  // Partial data loss should be reported.
+  EXPECT_THAT(initialize_result.initialize_stats().document_store_data_status(),
+              Eq(InitializeStatsProto::PARTIAL_LOSS));
+
+  // Verify that the persisted document can be retrieved.
+  GetResultProto expected_get_result_proto0;
+  expected_get_result_proto0.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto0.mutable_document() = document0;
+  EXPECT_THAT(another_icing.Get("namespace", "uri/0",
+                                GetResultSpecProto::default_instance()),
+              EqualsProto(expected_get_result_proto0));
+  // Verify that document in the unsynced tail cannot be retrieved.
+  EXPECT_THAT(
+      another_icing
+          .Get("namespace", "uri/1", GetResultSpecProto::default_instance())
+          .status(),
+      ProtoStatusIs(StatusProto::NOT_FOUND));
+}
+
+TEST_F(IcingSearchEngineInitializationTest,
+       ProtoLogNewHeaderFormat_switchFlagOffToOnShouldDiscardUnsyncedTail) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+
+  DocumentProto document0 =
+      DocumentBuilder()
+          .SetKey("namespace", "uri/0")
+          .SetSchema("Person")
+          .AddStringProperty("name", "person")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto document1 =
+      DocumentBuilder()
+          .SetKey("namespace", "uri/1")
+          .SetSchema("Person")
+          .AddStringProperty("name", "person")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_proto_log_new_header_format(false);
+  IcingSearchEngine icing(options, GetTestJniCache());
+
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+
+  // Put document0 and persist to disk.
+  ASSERT_THAT(icing.Put(document0).status(), ProtoIsOk());
+  ASSERT_THAT(icing.PersistToDisk(PersistType::FULL).status(), ProtoIsOk());
+
+  // Put document1 and don't persist to disk.
+  ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk());
+
+  // Initialize another Icing instance with the flag off.
+  IcingSearchEngineOptions another_options = GetDefaultIcingOptions();
+  another_options.set_enable_proto_log_new_header_format(true);
+  IcingSearchEngine another_icing(another_options, GetTestJniCache());
+
+  InitializeResultProto initialize_result = another_icing.Initialize();
+  // Partial data loss should be reported.
+  EXPECT_THAT(initialize_result.initialize_stats().document_store_data_status(),
+              Eq(InitializeStatsProto::PARTIAL_LOSS));
+
+  // Verify that the persisted document can be retrieved.
+  GetResultProto expected_get_result_proto0;
+  expected_get_result_proto0.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto0.mutable_document() = document0;
+  EXPECT_THAT(another_icing.Get("namespace", "uri/0",
+                                GetResultSpecProto::default_instance()),
+              EqualsProto(expected_get_result_proto0));
+  // Verify that document in the unsynced tail cannot be retrieved. Although we
+  // enable the flag, the existing data generated by the flag off instance
+  // cannot be recovered since the unsynced tail checksum doesn't match.
+  EXPECT_THAT(
+      another_icing
+          .Get("namespace", "uri/1", GetResultSpecProto::default_instance())
+          .status(),
+      ProtoStatusIs(StatusProto::NOT_FOUND));
+}
+
+TEST_F(IcingSearchEngineInitializationTest,
+       DeletePropagationFrom_switchFlagOffToOnShouldRevalidateDependency) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Label").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("target")
+                  .SetDataTypeJoinableString(
+                      JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                      DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  // Create 3 label documents with the following relationship:
+  // label1 -> label2 -> label3
+  DocumentProto label1 = DocumentBuilder()
+                             .SetKey("namespace", "label/1")
+                             .SetSchema("Label")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(10000)  // Expire at t = 10010 ms
+                             .Build();
+  DocumentProto label2 = DocumentBuilder()
+                             .SetKey("namespace", "label/2")
+                             .SetSchema("Label")
+                             .AddStringProperty("target", "namespace#label/1")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .Build();
+  DocumentProto label3 = DocumentBuilder()
+                             .SetKey("namespace", "label/3")
+                             .SetSchema("Label")
+                             .AddStringProperty("target", "namespace#label/2")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .Build();
+
+  {
+    IcingSearchEngineOptions options = GetDefaultIcingOptions();
+    options.set_enable_qualified_id_join_index_v3(true);
+    options.set_enable_soft_index_restoration(true);
+    options.set_enable_delete_propagation_from(false);
+    options.set_expired_document_purge_threshold_ms(0);
+
+    // Initialize Icing instance with the flag off and put all label documents.
+    auto fake_clock = std::make_unique<FakeClock>();
+    fake_clock->SetSystemTimeMilliseconds(10);
+
+    TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                                std::make_unique<IcingFilesystem>(),
+                                std::move(fake_clock), GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(label1).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(label2).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(label3).status(), ProtoIsOk());
+  }
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetSystemTimeMilliseconds(20000);
+
+  // Initialize Icing instance again with the flag on, at t = 20000 ms.
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  InitializeResultProto initialize_result = icing.Initialize();
+
+  // Join index should be rebuilt due to the flag change.
+  // - Label1 is expired at this moment.
+  // - Label2 is not expired, but delete propagation flag is enabled now and its
+  //   parent (label1) is expired, so when re-indexing it, it should fail and be
+  //   deleted.
+  // - Label3 is not expired and its parent (label2) is alive originally, so
+  //   when re-indexing it, it should succeed. But since label2 failed to be
+  //   re-indexed, Icing should delete label2 and propagate the deletion to
+  //   label3.
+  EXPECT_THAT(initialize_result.initialize_stats().document_store_data_status(),
+              Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result.initialize_stats().index_restoration_cause(),
+              Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(
+      initialize_result.initialize_stats().integer_index_restoration_cause(),
+      Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result.initialize_stats()
+                  .qualified_id_join_index_restoration_cause(),
+              Eq(InitializeStatsProto::FEATURE_FLAG_CHANGED));
+  EXPECT_THAT(
+      initialize_result.initialize_stats().embedding_index_restoration_cause(),
+      Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(
+      initialize_result.initialize_stats().num_failed_reindexed_documents(),
+      Eq(1));  // Label2 failed to be re-indexed.
+
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto1.mutable_status()->set_message(
+      "Document (namespace, label/1) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "label/1", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto1));
+
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_google::protobuf.mutable_status()->set_message(
+      "Document (namespace, label/2) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "label/2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_google::protobuf));
+
+  GetResultProto expected_get_result_proto3;
+  expected_get_result_proto3.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto3.mutable_status()->set_message(
+      "Document (namespace, label/3) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "label/3", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto3));
+}
+
+TEST_F(IcingSearchEngineInitializationTest,
+       DeletePropagationFrom_switchflagOnToOffShouldNotRevalidateDependency) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Label").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("target")
+                  .SetDataTypeJoinableString(
+                      JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                      DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  // Create 3 label documents with the following relationship:
+  // label1 -> label2 -> label3
+  DocumentProto label1 = DocumentBuilder()
+                             .SetKey("namespace", "label/1")
+                             .SetSchema("Label")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(10000)  // Expire at t = 10010 ms
+                             .Build();
+  DocumentProto label2 = DocumentBuilder()
+                             .SetKey("namespace", "label/2")
+                             .SetSchema("Label")
+                             .AddStringProperty("target", "namespace#label/1")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .Build();
+  DocumentProto label3 = DocumentBuilder()
+                             .SetKey("namespace", "label/3")
+                             .SetSchema("Label")
+                             .AddStringProperty("target", "namespace#label/2")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .Build();
+
+  {
+    IcingSearchEngineOptions options = GetDefaultIcingOptions();
+    options.set_enable_qualified_id_join_index_v3(true);
+    options.set_enable_soft_index_restoration(true);
+    options.set_enable_delete_propagation_from(true);
+    options.set_expired_document_purge_threshold_ms(0);
+
+    // Initialize Icing instance with the flag on and put all label documents.
+    auto fake_clock = std::make_unique<FakeClock>();
+    fake_clock->SetSystemTimeMilliseconds(10);
+
+    TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                                std::make_unique<IcingFilesystem>(),
+                                std::move(fake_clock), GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(label1).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(label2).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(label3).status(), ProtoIsOk());
+  }
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(false);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetSystemTimeMilliseconds(20000);
+
+  // Initialize Icing instance again with the flag off, at t = 20000 ms.
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  InitializeResultProto initialize_result = icing.Initialize();
+
+  // Join index should be rebuilt due to the flag change.
+  // - Label1 is expired at this moment.
+  // - Label2 is not expired. Although the schema sets delete propagation for
+  //   the joinable property, since the flag is off, we won't check the
+  //   dependency and it will be re-indexed successfully.
+  // - Label3 is re-indexed successfully.
+  EXPECT_THAT(initialize_result.initialize_stats().document_store_data_status(),
+              Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result.initialize_stats().index_restoration_cause(),
+              Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(
+      initialize_result.initialize_stats().integer_index_restoration_cause(),
+      Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(initialize_result.initialize_stats()
+                  .qualified_id_join_index_restoration_cause(),
+              Eq(InitializeStatsProto::FEATURE_FLAG_CHANGED));
+  EXPECT_THAT(
+      initialize_result.initialize_stats().embedding_index_restoration_cause(),
+      Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(
+      initialize_result.initialize_stats().num_failed_reindexed_documents(),
+      Eq(0));
+
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto1.mutable_status()->set_message(
+      "Document (namespace, label/1) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "label/1", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto1));
+
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_google::protobuf.mutable_document() = label2;
+  EXPECT_THAT(
+      icing.Get("namespace", "label/2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_google::protobuf));
+
+  GetResultProto expected_get_result_proto3;
+  expected_get_result_proto3.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto3.mutable_document() = label3;
+  EXPECT_THAT(
+      icing.Get("namespace", "label/3", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto3));
+}
+
 struct IcingSearchEngineInitializationVersionChangeTestParam {
   version_util::VersionInfo existing_version_info;
   std::unordered_set<IcingSearchEngineFeatureInfoProto::FlaggedFeatureType>
@@ -6092,23 +7335,29 @@
             filesystem(), GetBlobDir(), &fake_clock,
             /*orphan_blob_time_to_live_ms=*/0,
             PortableFileBackedProtoLog<BlobInfoProto>::kDefaultCompressionLevel,
-            /*manage_blob_files=*/true));
+            protobuf_ports::kDefaultMemLevel,
+            /*manage_blob_files=*/true, feature_flags_.get()));
 
     // Put message into DocumentStore
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(filesystem(), GetDocumentDir(), &fake_clock,
-                              schema_store.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            filesystem(), GetDocumentDir(), &fake_clock, schema_store.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     std::unique_ptr<DocumentStore> document_store =
         std::move(create_result.document_store);
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                               document_store->Put(message));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult put_result,
+        document_store->Put(document_util::CreateDocumentWrapper(message)));
     DocumentId doc_id = put_result.new_document_id;
 
     // Index doc_id with incorrect data
@@ -6117,7 +7366,8 @@
                            /*lite_index_sort_size=*/1024 * 8);
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<Index> index,
-        Index::Create(options, filesystem(), icing_filesystem()));
+        Index::Create(options, filesystem(), icing_filesystem(),
+                      feature_flags_.get()));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<IntegerIndex> integer_index,
@@ -6133,7 +7383,8 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<EmbeddingIndex> embedding_index,
         EmbeddingIndex::Create(filesystem(), GetEmbeddingIndexDir(),
-                               &fake_clock, feature_flags_.get()));
+                               &fake_clock, feature_flags_.get(),
+                               /*num_shards=*/32));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<TermIndexingHandler> term_indexing_handler,
@@ -6148,7 +7399,8 @@
         std::unique_ptr<QualifiedIdJoinIndexingHandler>
             qualified_id_join_indexing_handler,
         QualifiedIdJoinIndexingHandler::Create(
-            &fake_clock, document_store.get(), qualified_id_join_index.get()));
+            &fake_clock, document_store.get(), qualified_id_join_index.get(),
+            feature_flags_.get()));
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<EmbeddingIndexingHandler> embedding_indexing_handler,
         EmbeddingIndexingHandler::Create(&fake_clock, embedding_index.get(),
@@ -6174,8 +7426,10 @@
             .Build();
     ICING_ASSERT_OK_AND_ASSIGN(
         TokenizedDocument tokenized_document,
-        TokenizedDocument::Create(schema_store.get(), lang_segmenter_.get(),
-                                  std::move(incorrect_message)));
+        TokenizedDocument::Create(
+            schema_store.get(), lang_segmenter_.get(),
+            /*current_time_ms=*/fake_clock.GetSystemTimeMilliseconds(),
+            std::move(incorrect_message)));
     ICING_ASSERT_OK(index_processor.IndexDocument(tokenized_document, doc_id,
                                                   put_result.old_document_id));
 
@@ -6246,6 +7500,10 @@
       initialize_result.initialize_stats().embedding_index_restoration_cause(),
       Eq(expected_recovery_cause));
 
+  // needs_persist_type should be set to RECOVERY_PROOF.
+  EXPECT_THAT(initialize_result.needs_persist_type(),
+              Eq(PersistType::RECOVERY_PROOF));
+
   // Manually check version file
   ICING_ASSERT_OK_AND_ASSIGN(
       IcingSearchEngineVersionProto version_proto_after_init,
@@ -6814,6 +8072,146 @@
                     std::vector<bool>{true, true, true, true},
                     std::vector<bool>{false, false, false, false}));
 
+class IcingSearchEngineInitializationChangeEmbeddingIndexNumShardsTest
+    : public IcingSearchEngineInitializationTest,
+      public ::testing::WithParamInterface<std::vector<uint32_t>> {};
+
+TEST_P(IcingSearchEngineInitializationChangeEmbeddingIndexNumShardsTest,
+       ChangeEmbeddingIndexNumShardsTest) {
+  constexpr float eps = 0.0001f;
+  std::vector<uint32_t> num_shards = GetParam();
+
+  // Create icing and add 128 documents in different schemas, so that the
+  // embeddings will be stored in different shards.
+  SchemaBuilder schema = SchemaBuilder();
+  std::vector<DocumentProto> documents;
+  documents.reserve(128);
+  for (int i = 0; i < 128; ++i) {
+    schema.AddType(
+        SchemaTypeConfigBuilder()
+            .SetType("Type" + std::to_string(i))
+            .AddProperty(
+                PropertyConfigBuilder()
+                    .SetName("embeddingUnquantized")
+                    .SetDataTypeVector(
+                        EMBEDDING_INDEXING_LINEAR_SEARCH,
+                        EmbeddingIndexingConfig::QuantizationType::NONE)
+                    .SetCardinality(CARDINALITY_OPTIONAL))
+            .AddProperty(
+                PropertyConfigBuilder()
+                    .SetName("embeddingQuantized")
+                    .SetDataTypeVector(EMBEDDING_INDEXING_LINEAR_SEARCH,
+                                       EmbeddingIndexingConfig::
+                                           QuantizationType::QUANTIZE_8_BIT)
+                    .SetCardinality(CARDINALITY_OPTIONAL)));
+    PropertyProto::VectorProto vector =
+        CreateVector("my_model", {0., 0., (float)i});
+    documents.push_back(DocumentBuilder()
+                            .SetKey("icing", "uri" + std::to_string(i))
+                            .SetSchema("Type" + std::to_string(i))
+                            .SetCreationTimestampMs(1)
+                            .AddVectorProperty("embeddingQuantized", vector)
+                            .AddVectorProperty("embeddingUnquantized", vector)
+                            .Build());
+  }
+  {
+    IcingSearchEngineOptions options = GetDefaultIcingOptions();
+    options.set_enable_embedding_index(true);
+    options.set_enable_embedding_quantization(true);
+    options.set_embedding_index_num_shards(num_shards[0]);
+    TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                                std::make_unique<IcingFilesystem>(),
+                                std::make_unique<FakeClock>(),
+                                GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing.SetSchema(schema.Build()).status(), ProtoIsOk());
+    for (const auto& document : documents) {
+      ASSERT_THAT(icing.Put(document).status(), ProtoIsOk());
+    }
+  }
+
+  // Create icing multiple times with different num_shards.
+  for (int i = 1; i < num_shards.size(); ++i) {
+    bool num_shards_changed = num_shards[i] != num_shards[i - 1];
+
+    // Ensure that the embedding index is rebuilt if num_shards is changed.
+    auto mock_filesystem = std::make_unique<MockFilesystem>();
+    EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(_))
+        .WillRepeatedly(DoDefault());
+    EXPECT_CALL(*mock_filesystem,
+                DeleteDirectoryRecursively(EndsWith("/embedding_index_dir")))
+        .Times(num_shards_changed ? 1 : 0);
+
+    IcingSearchEngineOptions options = GetDefaultIcingOptions();
+    options.set_enable_embedding_index(true);
+    options.set_enable_embedding_quantization(true);
+    options.set_embedding_index_num_shards(num_shards[i]);
+    TestIcingSearchEngine icing(options, std::move(mock_filesystem),
+                                std::make_unique<IcingFilesystem>(),
+                                std::make_unique<FakeClock>(),
+                                GetTestJniCache());
+    InitializeResultProto initialize_result = icing.Initialize();
+    ASSERT_THAT(initialize_result.status(), ProtoIsOk());
+    // Ensure that the embedding index is rebuilt if num_shards is changed.
+    EXPECT_THAT(initialize_result.initialize_stats()
+                    .embedding_index_restoration_cause(),
+                Eq(num_shards_changed ? InitializeStatsProto::IO_ERROR
+                                      : InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result.initialize_stats().index_restoration_cause(),
+                Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(
+        initialize_result.initialize_stats().integer_index_restoration_cause(),
+        Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result.initialize_stats()
+                    .qualified_id_join_index_restoration_cause(),
+                Eq(InitializeStatsProto::NONE));
+
+    // Write an embedding query that always matches all the documents.
+    SearchSpecProto search_spec;
+    search_spec.set_query("semanticSearch(getEmbeddingParameter(0))");
+    *search_spec.add_embedding_query_vectors() =
+        CreateVector("my_model", {1, 1, 1});
+    search_spec.set_embedding_query_metric_type(
+        SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT);
+    search_spec.add_enabled_features(
+        std::string(kListFilterQueryLanguageFeature));
+    ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+    scoring_spec.set_rank_by(
+        ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+    // Set the advanced scoring expression to a constant so that the documents
+    // will be returned according to their order they were added.
+    scoring_spec.set_advanced_scoring_expression("0");
+    // Set the additional advanced scoring expression to check the embedding
+    // scores.
+    // Each embedding in document[i] should have a score of i, and every
+    // document has two embeddings, one quantized and one unquantized. So
+    // document[i] should have a total score of i * 2.
+    scoring_spec.add_additional_advanced_scoring_expressions(
+        "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))");
+    ResultSpecProto result_spec;
+    result_spec.set_num_per_page(10000);
+    SearchResultProto results =
+        icing.Search(search_spec, scoring_spec, result_spec);
+    ASSERT_THAT(results.status(), ProtoIsOk());
+    ASSERT_THAT(results.results(), SizeIs(128));
+    for (int i = 0; i < 128; ++i) {
+      EXPECT_THAT(results.results(127 - i).document(),
+                  EqualsProto(documents[i]));
+      EXPECT_NEAR(results.results(127 - i).additional_scores(0), i * 2.0f, eps);
+    }
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    IcingSearchEngineInitializationChangeEmbeddingIndexNumShardsTest,
+    IcingSearchEngineInitializationChangeEmbeddingIndexNumShardsTest,
+    testing::Values(std::vector<uint32_t>{1, 32, 1, 32, 32, 32, 1, 1, 32},
+                    std::vector<uint32_t>{32, 1, 32, 1, 1, 1, 32, 32, 1},
+                    std::vector<uint32_t>{1, 2, 4, 8, 16, 32, 1, 1, 32},
+                    std::vector<uint32_t>{32, 16, 8, 4, 2, 1, 32, 32, 1},
+                    std::vector<uint32_t>{1, 1, 1, 1},
+                    std::vector<uint32_t>{32, 32, 32, 32}));
+
 class IcingSearchEngineInitializationChangeEnableScorablePropertiesFlagTest
     : public IcingSearchEngineInitializationTest,
       public ::testing::WithParamInterface<std::vector<bool>> {};
@@ -6885,6 +8283,9 @@
         initialize_result.initialize_stats().document_store_recovery_cause(),
         Eq(flag_changed ? InitializeStatsProto::FEATURE_FLAG_CHANGED
                         : InitializeStatsProto::NONE));
+    EXPECT_THAT(
+        initialize_result.needs_persist_type(),
+        Eq(flag_changed ? PersistType::RECOVERY_PROOF : PersistType::UNKNOWN));
 
     // Schema store and all indices should be unaffected.
     EXPECT_THAT(
@@ -7005,9 +8406,9 @@
                            .SetCardinality(CARDINALITY_OPTIONAL))
           .Build();
   SchemaTypeConfigProto db1_email_type_with_db =
-      SchemaTypeConfigBuilder(db1_email_type).SetDatabase("db1").Build();
+      SchemaTypeConfigBuilder(db1_email_type).SetDatabase("db1/").Build();
   SchemaTypeConfigProto db2_email_type_with_db =
-      SchemaTypeConfigBuilder(db2_email_type).SetDatabase("db2").Build();
+      SchemaTypeConfigBuilder(db2_email_type).SetDatabase("db2/").Build();
 
   SchemaProto previous_version_db1_schema;
   SchemaProto previous_version_db2_schema;
@@ -7075,18 +8476,12 @@
     EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
     // 1. Set schema.
     if (options.enable_schema_database()) {
-      // Can only set schema for a single database at a time.
+      // Use the SetSchemaRequestProto with empty database field to populate
+      // both databases at once.
       ASSERT_THAT(icing
                       .SetSchema(CreateSetSchemaRequestProto(
-                          previous_version_db1_schema,
-                          previous_version_db1_schema.types(0).database(),
-                          /*ignore_errors_and_delete_documents=*/false))
-                      .status(),
-                  ProtoIsOk());
-      ASSERT_THAT(icing
-                      .SetSchema(CreateSetSchemaRequestProto(
-                          previous_version_db2_schema,
-                          previous_version_db2_schema.types(0).database(),
+                          previous_version_schema,
+                          /*database=*/"",
                           /*ignore_errors_and_delete_documents=*/false))
                       .status(),
                   ProtoIsOk());
@@ -7166,9 +8561,9 @@
   // Verify GetSchema
   if (enable_schema_database) {
     SchemaTypeConfigProto db1_email_type_with_db =
-        SchemaTypeConfigBuilder(db1_email_type).SetDatabase("db1").Build();
+        SchemaTypeConfigBuilder(db1_email_type).SetDatabase("db1/").Build();
     SchemaTypeConfigProto db2_email_type_with_db =
-        SchemaTypeConfigBuilder(db2_email_type).SetDatabase("db2").Build();
+        SchemaTypeConfigBuilder(db2_email_type).SetDatabase("db2/").Build();
     SchemaProto full_schema_with_database = SchemaBuilder()
                                                 .AddType(db1_email_type_with_db)
                                                 .AddType(db2_email_type_with_db)
@@ -7190,14 +8585,14 @@
     expected_get_schema_result_proto_db1.mutable_status()->set_code(
         StatusProto::OK);
     *expected_get_schema_result_proto_db1.mutable_schema() = db1_schema;
-    EXPECT_THAT(icing.GetSchema("db1"),
+    EXPECT_THAT(icing.GetSchema("db1/"),
                 EqualsProto(expected_get_schema_result_proto_db1));
 
     GetSchemaResultProto expected_get_schema_result_proto_db2;
     expected_get_schema_result_proto_db2.mutable_status()->set_code(
         StatusProto::OK);
     *expected_get_schema_result_proto_db2.mutable_schema() = db2_schema;
-    EXPECT_THAT(icing.GetSchema("db2"),
+    EXPECT_THAT(icing.GetSchema("db2/"),
                 EqualsProto(expected_get_schema_result_proto_db2));
   } else {
     GetSchemaResultProto expected_get_schema_result_proto;
@@ -7337,6 +8732,9 @@
                     .qualified_id_join_index_restoration_cause(),
                 Eq(flag_changed ? InitializeStatsProto::FEATURE_FLAG_CHANGED
                                 : InitializeStatsProto::NONE));
+    EXPECT_THAT(
+        initialize_result.needs_persist_type(),
+        Eq(flag_changed ? PersistType::RECOVERY_PROOF : PersistType::UNKNOWN));
 
     // Schema store, document store and all other indices should be unaffected.
     EXPECT_THAT(
@@ -7405,6 +8803,261 @@
                     std::vector<bool>{true, true, true, true},
                     std::vector<bool>{false, false, false, false}));
 
+class IcingSearchEngineInitializationChangeNonExistentQualifiedIdJoinFlagTest
+    : public IcingSearchEngineInitializationTest,
+      public ::testing::WithParamInterface<std::vector<bool>> {};
+TEST_P(IcingSearchEngineInitializationChangeNonExistentQualifiedIdJoinFlagTest,
+       ChangeNonExistentQualifiedIdJoinFlagTest) {
+  std::vector<bool> enable_non_existent_qualified_id_join_flags = GetParam();
+
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Message")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("body")
+                                        .SetDataTypeString(TERM_MATCH_PREFIX,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("senderQualifiedId")
+                                        .SetDataTypeJoinableString(
+                                            JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                        .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+
+  DocumentProto person =
+      DocumentBuilder()
+          .SetKey("namespace", "person")
+          .SetSchema("Person")
+          .AddStringProperty("name", "person")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto message =
+      DocumentBuilder()
+          .SetKey("namespace", "message/1")
+          .SetSchema("Message")
+          .AddStringProperty("body", "message body")
+          .AddStringProperty("senderQualifiedId", "namespace#person")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+
+  {
+    IcingSearchEngineOptions options = GetDefaultIcingOptions();
+    options.set_enable_non_existent_qualified_id_join(
+        enable_non_existent_qualified_id_join_flags.at(0));
+    TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                                std::make_unique<IcingFilesystem>(),
+                                std::make_unique<FakeClock>(),
+                                GetTestJniCache());
+
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(person).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(message).status(), ProtoIsOk());
+  }
+
+  // Create icing multiple times with different
+  // enable_non_existent_qualified_id_join flags.
+  for (int i = 1; i < enable_non_existent_qualified_id_join_flags.size(); ++i) {
+    bool flag_changed = enable_non_existent_qualified_id_join_flags[i] !=
+                        enable_non_existent_qualified_id_join_flags[i - 1];
+
+    // Ensure that the qualified id join index is rebuilt if the flag is
+    // changed.
+    IcingSearchEngineOptions options = GetDefaultIcingOptions();
+    options.set_enable_non_existent_qualified_id_join(
+        enable_non_existent_qualified_id_join_flags[i]);
+    TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                                std::make_unique<IcingFilesystem>(),
+                                std::make_unique<FakeClock>(),
+                                GetTestJniCache());
+    InitializeResultProto initialize_result = icing.Initialize();
+    ASSERT_THAT(initialize_result.status(), ProtoIsOk());
+
+    // Qualified id join index recovery cause should be FEATURE_FLAG_CHANGED if
+    // flag is changed.
+    EXPECT_THAT(initialize_result.initialize_stats()
+                    .qualified_id_join_index_restoration_cause(),
+                Eq(flag_changed ? InitializeStatsProto::FEATURE_FLAG_CHANGED
+                                : InitializeStatsProto::NONE));
+    EXPECT_THAT(
+        initialize_result.needs_persist_type(),
+        Eq(flag_changed ? PersistType::RECOVERY_PROOF : PersistType::UNKNOWN));
+
+    // Schema store, document store and all other indices should be unaffected.
+    EXPECT_THAT(
+        initialize_result.initialize_stats().schema_store_recovery_cause(),
+        Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(
+        initialize_result.initialize_stats().document_store_recovery_cause(),
+        Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result.initialize_stats().index_restoration_cause(),
+                Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(
+        initialize_result.initialize_stats().integer_index_restoration_cause(),
+        Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result.initialize_stats()
+                    .embedding_index_restoration_cause(),
+                Eq(InitializeStatsProto::NONE));
+
+    // Prepare join search spec to join a query for `name:person` with a child
+    // query for `body:message` based on the child's `senderQualifiedId` field.
+    //
+    // No matter what the flag value is, the join API should always return the
+    // expected result.
+    SearchSpecProto search_spec;
+    search_spec.set_term_match_type(TermMatchType::EXACT_ONLY);
+    search_spec.set_query("name:person");
+    JoinSpecProto* join_spec = search_spec.mutable_join_spec();
+    join_spec->set_parent_property_expression(
+        std::string(JoinProcessor::kQualifiedIdExpr));
+    join_spec->set_child_property_expression("senderQualifiedId");
+    join_spec->set_aggregation_scoring_strategy(
+        JoinSpecProto::AggregationScoringStrategy::COUNT);
+    JoinSpecProto::NestedSpecProto* nested_spec =
+        join_spec->mutable_nested_spec();
+    SearchSpecProto* nested_search_spec = nested_spec->mutable_search_spec();
+    nested_search_spec->set_term_match_type(TermMatchType::EXACT_ONLY);
+    nested_search_spec->set_query("body:message");
+    *nested_spec->mutable_scoring_spec() = GetDefaultScoringSpec();
+    *nested_spec->mutable_result_spec() = ResultSpecProto::default_instance();
+
+    ResultSpecProto result_spec = ResultSpecProto::default_instance();
+    result_spec.set_max_joined_children_per_parent_to_return(
+        std::numeric_limits<int32_t>::max());
+
+    SearchResultProto expected_search_result_proto;
+    expected_search_result_proto.mutable_status()->set_code(StatusProto::OK);
+    SearchResultProto::ResultProto* result_proto =
+        expected_search_result_proto.mutable_results()->Add();
+    *result_proto->mutable_document() = person;
+    *result_proto->mutable_joined_results()->Add()->mutable_document() =
+        message;
+
+    SearchResultProto search_result_proto =
+        icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
+    EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
+                                         expected_search_result_proto));
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    IcingSearchEngineInitializationChangeNonExistentQualifiedIdJoinFlagTest,
+    IcingSearchEngineInitializationChangeNonExistentQualifiedIdJoinFlagTest,
+    testing::Values(std::vector<bool>{false, true, false, true, false, true},
+                    std::vector<bool>{true, false, true, false, true, false},
+                    std::vector<bool>{false, true, true, true, false, true},
+                    std::vector<bool>{true, false, false, false, true, false},
+                    std::vector<bool>{true, true, true, true},
+                    std::vector<bool>{false, false, false, false}));
+
+class IcingSearchEngineInitializationChangeEnableProtoLogNewHeaderFormatFlagTest
+    : public IcingSearchEngineInitializationTest,
+      public ::testing::WithParamInterface<std::vector<bool>> {};
+
+TEST_P(
+    IcingSearchEngineInitializationChangeEnableProtoLogNewHeaderFormatFlagTest,
+    ChangeFlag) {
+  std::vector<bool> enable_proto_log_new_header_format_flags = GetParam();
+
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+
+  DocumentProto document =
+      DocumentBuilder()
+          .SetKey("namespace", "uri")
+          .SetSchema("Person")
+          .AddStringProperty("name", "person")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+
+  {
+    IcingSearchEngineOptions options = GetDefaultIcingOptions();
+    options.set_enable_proto_log_new_header_format(
+        enable_proto_log_new_header_format_flags[0]);
+    IcingSearchEngine icing(options, GetTestJniCache());
+
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(document).status(), ProtoIsOk());
+    ASSERT_THAT(icing.PersistToDisk(PersistType::FULL).status(), ProtoIsOk());
+  }
+
+  // Create icing multiple times with different
+  // enable_proto_log_new_header_format flags.
+  for (int i = 1; i < enable_proto_log_new_header_format_flags.size(); ++i) {
+    // Ensure that persisted data is kept.
+    IcingSearchEngineOptions options = GetDefaultIcingOptions();
+    options.set_enable_proto_log_new_header_format(
+        enable_proto_log_new_header_format_flags[i]);
+    IcingSearchEngine icing(options, GetTestJniCache());
+
+    InitializeResultProto initialize_result = icing.Initialize();
+    ASSERT_THAT(initialize_result.status(), ProtoIsOk());
+
+    // No data loss should be reported.
+    EXPECT_THAT(
+        initialize_result.initialize_stats().document_store_data_status(),
+        Eq(InitializeStatsProto::NONE));
+
+    // We don't really care about the derived files recovery cause, but normally
+    // this flag change should not affect derived files recovery, so let's check
+    // it here.
+    EXPECT_THAT(
+        initialize_result.initialize_stats().schema_store_recovery_cause(),
+        Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(
+        initialize_result.initialize_stats().document_store_recovery_cause(),
+        Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result.initialize_stats().index_restoration_cause(),
+                Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(
+        initialize_result.initialize_stats().integer_index_restoration_cause(),
+        Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result.initialize_stats()
+                    .qualified_id_join_index_restoration_cause(),
+                Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result.initialize_stats()
+                    .embedding_index_restoration_cause(),
+                Eq(InitializeStatsProto::NONE));
+
+    // PersistToDisk is not needed.
+    EXPECT_THAT(initialize_result.needs_persist_type(),
+                Eq(PersistType::UNKNOWN));
+
+    // Verify that the persisted document can be retrieved.
+    GetResultProto expected_get_result_proto;
+    expected_get_result_proto.mutable_status()->set_code(StatusProto::OK);
+    *expected_get_result_proto.mutable_document() = document;
+
+    EXPECT_THAT(
+        icing.Get("namespace", "uri", GetResultSpecProto::default_instance()),
+        EqualsProto(expected_get_result_proto));
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    IcingSearchEngineInitializationChangeEnableProtoLogNewHeaderFormatFlagTest,
+    IcingSearchEngineInitializationChangeEnableProtoLogNewHeaderFormatFlagTest,
+    testing::Values(std::vector<bool>{false, true, false, true, false, true},
+                    std::vector<bool>{true, false, true, false, true, false},
+                    std::vector<bool>{false, true, true, true, false, true},
+                    std::vector<bool>{true, false, false, false, true, false},
+                    std::vector<bool>{true, true, true, true},
+                    std::vector<bool>{false, false, false, false}));
+
 }  // namespace
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/icing-search-engine_optimize_test.cc b/icing/icing-search-engine_optimize_test.cc
index cd36074..d8018db 100644
--- a/icing/icing-search-engine_optimize_test.cc
+++ b/icing/icing-search-engine_optimize_test.cc
@@ -75,6 +75,7 @@
 using ::testing::Gt;
 using ::testing::HasSubstr;
 using ::testing::Lt;
+using ::testing::Ne;
 using ::testing::Return;
 using ::testing::SizeIs;
 
@@ -230,25 +231,28 @@
       document2;
   SearchResultProto search_result_proto =
       icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
-  EXPECT_THAT(search_result_proto.next_page_token(), Gt(kInvalidNextPageToken));
   uint64_t next_page_token = search_result_proto.next_page_token();
+
   // Since the token is a random number, we don't need to verify
   expected_search_result_proto.set_next_page_token(next_page_token);
+  EXPECT_THAT(search_result_proto.next_page_token(), Ne(kInvalidNextPageToken));
   EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
                                        expected_search_result_proto));
-  // Now document1 is still to be fetched.
 
+  // Now there are more pages to be fetched (document1). Call Optimize.
   OptimizeResultProto optimize_result_proto;
   optimize_result_proto.mutable_status()->set_code(StatusProto::OK);
   optimize_result_proto.mutable_status()->set_message("");
   OptimizeResultProto actual_result = icing.Optimize();
   actual_result.clear_optimize_stats();
+  actual_result.clear_vm_binder_transaction_latency_start_time_ms();
   ASSERT_THAT(actual_result, EqualsProto(optimize_result_proto));
 
   // Tries to fetch the second page, no results since all tokens have been
-  // invalidated during Optimize()
+  // invalidated during Optimize().
   expected_search_result_proto.clear_results();
   expected_search_result_proto.clear_next_page_token();
+  expected_search_result_proto.set_page_token_not_found(true);
   search_result_proto = icing.GetNextPage(next_page_token);
   EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
                                        expected_search_result_proto));
@@ -361,10 +365,17 @@
           .AddStringProperty("body", "message body one")
           .SetCreationTimestampMs(kDefaultCreationTimestampMs)
           .Build();
-  DocumentProto document2 = DocumentBuilder()
-                                .SetKey("namespace", "uri2")
+  DocumentProto document2 =
+      DocumentBuilder()
+          .SetKey("namespace", "uri2")
+          .SetSchema("Message")
+          .AddStringProperty("body", "message body two")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto document3 = DocumentBuilder()
+                                .SetKey("namespace", "uri3")
                                 .SetSchema("Message")
-                                .AddStringProperty("body", "message body two")
+                                .AddStringProperty("body", "message body three")
                                 .SetCreationTimestampMs(100)
                                 .SetTtlMs(500)
                                 .Build();
@@ -385,9 +396,13 @@
     EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0));
     EXPECT_THAT(optimize_info.estimated_optimizable_bytes(), Eq(0));
     EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(0));
+    EXPECT_TRUE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
 
+    // Set schema and add 2 documents.
     ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
     ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk());
 
     // Only have active documents, nothing is optimizable yet.
     optimize_info = icing.GetOptimizeInfo();
@@ -395,32 +410,64 @@
     EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0));
     EXPECT_THAT(optimize_info.estimated_optimizable_bytes(), Eq(0));
     EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(0));
+    EXPECT_TRUE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
 
-    // Deletes document1
-    ASSERT_THAT(icing.Delete("namespace", "uri1").status(), ProtoIsOk());
+    // Send a search request to create a result state.
+    SearchSpecProto search_spec;
+    search_spec.set_query("body:message");
+    search_spec.set_term_match_type(TermMatchType::EXACT_ONLY);
+    ResultSpecProto result_spec;
+    result_spec.set_num_per_page(1);
+    SearchResultProto search_result =
+        icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
+    ASSERT_THAT(search_result.status(), ProtoIsOk());
+    ASSERT_THAT(search_result.results_size(), Eq(1));
+    ASSERT_THAT(search_result.next_page_token(), Ne(kInvalidNextPageToken));
 
     optimize_info = icing.GetOptimizeInfo();
     EXPECT_THAT(optimize_info.status(), ProtoIsOk());
-    EXPECT_THAT(optimize_info.optimizable_docs(), Eq(1));
-    EXPECT_THAT(optimize_info.estimated_optimizable_bytes(), Gt(0));
+    EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0));
+    EXPECT_THAT(optimize_info.estimated_optimizable_bytes(), Eq(0));
     EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(0));
-    int64_t first_estimated_optimizable_bytes =
-        optimize_info.estimated_optimizable_bytes();
+    EXPECT_TRUE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(1));
 
-    // Add a second document, but it'll be expired since the time (1000) is
-    // greater than the document's creation timestamp (100) + the document's ttl
-    // (500)
-    ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk());
+    // Deletes document1 and document2.
+    ASSERT_THAT(icing.Delete("namespace", "uri1").status(), ProtoIsOk());
+    ASSERT_THAT(icing.Delete("namespace", "uri2").status(), ProtoIsOk());
 
     optimize_info = icing.GetOptimizeInfo();
     EXPECT_THAT(optimize_info.status(), ProtoIsOk());
     EXPECT_THAT(optimize_info.optimizable_docs(), Eq(2));
+    EXPECT_THAT(optimize_info.estimated_optimizable_bytes(), Gt(0));
+    EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(0));
+    EXPECT_TRUE(optimize_info.no_previous_optimize_info());
+    int64_t first_estimated_optimizable_bytes =
+        optimize_info.estimated_optimizable_bytes();
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(1));
+
+    // Add third document, but it'll be expired since the time (1000) is
+    // greater than the document's creation timestamp (100) + the document's ttl
+    // (500)
+    ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk());
+
+    optimize_info = icing.GetOptimizeInfo();
+    EXPECT_THAT(optimize_info.status(), ProtoIsOk());
+    EXPECT_THAT(optimize_info.optimizable_docs(), Eq(3));
     EXPECT_THAT(optimize_info.estimated_optimizable_bytes(),
                 Gt(first_estimated_optimizable_bytes));
     EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(0));
+    EXPECT_TRUE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(1));
 
     // Optimize
     ASSERT_THAT(icing.Optimize().status(), ProtoIsOk());
+
+    // Result state manager is reset after optimize.
+    optimize_info = icing.GetOptimizeInfo();
+    EXPECT_THAT(optimize_info.status(), ProtoIsOk());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
   }
 
   {
@@ -440,6 +487,125 @@
     EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0));
     EXPECT_THAT(optimize_info.estimated_optimizable_bytes(), Eq(0));
     EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(4000));
+    EXPECT_FALSE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
+  }
+}
+
+TEST_F(IcingSearchEngineOptimizeTest,
+       NegativeTimeSinceLastOptimizeResetsToZero) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Message").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("body")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+
+  DocumentProto document1 =
+      DocumentBuilder()
+          .SetKey("namespace", "uri1")
+          .SetSchema("Message")
+          .AddStringProperty("body", "message body one")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("namespace", "uri2")
+                                .SetSchema("Message")
+                                .AddStringProperty("body", "message body two")
+                                .SetCreationTimestampMs(100)
+                                .SetTtlMs(500)
+                                .Build();
+
+  IcingSearchEngineOptions icing_options = GetDefaultIcingOptions();
+  icing_options.set_calculate_time_since_last_attempted_optimize(true);
+  {
+    auto fake_clock = std::make_unique<FakeClock>();
+    fake_clock->SetSystemTimeMilliseconds(1000);
+    TestIcingSearchEngine icing(icing_options, std::make_unique<Filesystem>(),
+                                std::make_unique<IcingFilesystem>(),
+                                std::move(fake_clock), GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+    // Just initialized, nothing is optimizable yet.
+    GetOptimizeInfoResultProto optimize_info = icing.GetOptimizeInfo();
+    EXPECT_THAT(optimize_info.status(), ProtoIsOk());
+    EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0));
+    EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(0));
+    EXPECT_TRUE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
+
+    // Call some APIs
+    ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Delete("namespace", "uri1").status(), ProtoIsOk());
+    // Add a second document, but it'll be expired since the time (1000) is
+    // greater than the document's creation timestamp (100) + the document's ttl
+    // (500)
+    ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk());
+
+    optimize_info = icing.GetOptimizeInfo();
+    EXPECT_THAT(optimize_info.status(), ProtoIsOk());
+    EXPECT_THAT(optimize_info.optimizable_docs(), Eq(2));
+    EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(0));
+    EXPECT_TRUE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
+
+    // Optimize
+    OptimizeResultProto optimize_result = icing.Optimize();
+    EXPECT_THAT(optimize_result.status(), ProtoIsOk());
+    EXPECT_THAT(optimize_result.optimize_stats().time_since_last_optimize_ms(),
+                Eq(0));
+    EXPECT_THAT(optimize_result.optimize_stats()
+                    .time_since_last_successful_optimize_ms(),
+                Eq(0));
+  }
+
+  {
+    // Recreate with new time that's earlier than the last successful optimize
+    // run time. no_previous_optimize_info should be true and time since last
+    // optimize values should be negative.
+    auto fake_clock = std::make_unique<FakeClock>();
+    fake_clock->SetSystemTimeMilliseconds(500);
+    TestIcingSearchEngine icing(icing_options, std::make_unique<Filesystem>(),
+                                std::make_unique<IcingFilesystem>(),
+                                std::move(fake_clock), GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+    GetOptimizeInfoResultProto optimize_info = icing.GetOptimizeInfo();
+    EXPECT_THAT(optimize_info.status(), ProtoIsOk());
+    EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0));
+    EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(-500));
+    EXPECT_TRUE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
+
+    // Optimize.
+    OptimizeResultProto optimize_result = icing.Optimize();
+    EXPECT_THAT(optimize_result.status(), ProtoIsOk());
+    EXPECT_THAT(optimize_result.optimize_stats().time_since_last_optimize_ms(),
+                Eq(-500));
+    EXPECT_THAT(optimize_result.optimize_stats()
+                    .time_since_last_successful_optimize_ms(),
+                Eq(-500));
+  }
+
+  {
+    // Recreate with new timer and check that time_since_last_optimize_ms is
+    // populated correctly.
+    auto fake_clock = std::make_unique<FakeClock>();
+    fake_clock->SetSystemTimeMilliseconds(800);
+    TestIcingSearchEngine icing(icing_options, std::make_unique<Filesystem>(),
+                                std::make_unique<IcingFilesystem>(),
+                                std::move(fake_clock), GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+    GetOptimizeInfoResultProto optimize_info = icing.GetOptimizeInfo();
+    EXPECT_THAT(optimize_info.status(), ProtoIsOk());
+    EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0));
+    EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(300));
+    EXPECT_FALSE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
   }
 }
 
@@ -487,6 +653,8 @@
     EXPECT_THAT(optimize_info.status(), ProtoIsOk());
     EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0));
     EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(0));
+    EXPECT_TRUE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
 
     // Call some APIs
     ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
@@ -501,6 +669,8 @@
     EXPECT_THAT(optimize_info.status(), ProtoIsOk());
     EXPECT_THAT(optimize_info.optimizable_docs(), Eq(2));
     EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(0));
+    EXPECT_TRUE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
 
     // Optimize
     OptimizeResultProto optimize_result = icing.Optimize();
@@ -536,6 +706,8 @@
     EXPECT_THAT(optimize_info.status(), ProtoIsOk());
     EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0));
     EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(500));
+    EXPECT_FALSE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
 
     // Optimize again -- this should fail because of the mock filesystem.
     OptimizeResultProto optimize_result = icing.Optimize();
@@ -567,6 +739,8 @@
     EXPECT_THAT(optimize_info.status(), ProtoIsOk());
     EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0));
     EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(1300));
+    EXPECT_FALSE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
 
     // Optimize again -- this should fail because of the mock filesystem, but
     // the time since last optimize should be populated.
@@ -598,6 +772,8 @@
     EXPECT_THAT(optimize_info.status(), ProtoIsOk());
     EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0));
     EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(2700));
+    EXPECT_FALSE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
 
     // Optimize
     OptimizeResultProto optimize_result = icing.Optimize();
@@ -655,6 +831,8 @@
     EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0));
     EXPECT_THAT(optimize_info.estimated_optimizable_bytes(), Eq(0));
     EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(0));
+    EXPECT_TRUE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
 
     // Call some APIs
     ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
@@ -669,6 +847,8 @@
     EXPECT_THAT(optimize_info.status(), ProtoIsOk());
     EXPECT_THAT(optimize_info.optimizable_docs(), Eq(2));
     EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(0));
+    EXPECT_TRUE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
 
     // Optimize
     OptimizeResultProto optimize_result = icing.Optimize();
@@ -704,6 +884,8 @@
     EXPECT_THAT(optimize_info.status(), ProtoIsOk());
     EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0));
     EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(500));
+    EXPECT_FALSE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
 
     // Optimize again -- this should fail because of the mock filesystem.
     OptimizeResultProto optimize_result = icing.Optimize();
@@ -740,6 +922,8 @@
     EXPECT_THAT(optimize_info.status(), ProtoIsOk());
     EXPECT_THAT(optimize_info.optimizable_docs(), Eq(0));
     EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(1300));
+    EXPECT_FALSE(optimize_info.no_previous_optimize_info());
+    EXPECT_THAT(optimize_info.num_active_result_states(), Eq(0));
 
     // Optimize again -- this should fail because of the mock filesystem.
     OptimizeResultProto optimize_result = icing.Optimize();
@@ -764,6 +948,7 @@
     GetOptimizeInfoResultProto optimize_info = icing.GetOptimizeInfo();
     EXPECT_THAT(optimize_info.status(), ProtoIsOk());
     EXPECT_THAT(optimize_info.time_since_last_optimize_ms(), Eq(4000));
+    EXPECT_FALSE(optimize_info.no_previous_optimize_info());
 
     // Optimize
     OptimizeResultProto optimize_result = icing.Optimize();
@@ -1939,6 +2124,8 @@
               Ge(result.optimize_stats().storage_size_after() - page_size));
   result.mutable_optimize_stats()->clear_storage_size_before();
   result.mutable_optimize_stats()->clear_storage_size_after();
+  result.mutable_optimize_stats()->clear_before_optimize_persist_stats();
+  result.mutable_optimize_stats()->clear_after_optimize_persist_stats();
   EXPECT_THAT(result.optimize_stats(), EqualsProto(expected));
 
   fake_clock = std::make_unique<FakeClock>();
@@ -1969,6 +2156,8 @@
               Eq(result.optimize_stats().storage_size_after()));
   result.mutable_optimize_stats()->clear_storage_size_before();
   result.mutable_optimize_stats()->clear_storage_size_after();
+  result.mutable_optimize_stats()->clear_before_optimize_persist_stats();
+  result.mutable_optimize_stats()->clear_after_optimize_persist_stats();
   EXPECT_THAT(result.optimize_stats(), EqualsProto(expected));
 
   // Delete the last document.
@@ -1995,6 +2184,8 @@
               Ge(result.optimize_stats().storage_size_after()));
   result.mutable_optimize_stats()->clear_storage_size_before();
   result.mutable_optimize_stats()->clear_storage_size_after();
+  result.mutable_optimize_stats()->clear_before_optimize_persist_stats();
+  result.mutable_optimize_stats()->clear_after_optimize_persist_stats();
   EXPECT_THAT(result.optimize_stats(), EqualsProto(expected));
 }
 
@@ -2059,18 +2250,76 @@
   // Delete the first document.
   ASSERT_THAT(icing->Delete(document1.namespace_(), document1.uri()).status(),
               ProtoIsOk());
-  ASSERT_THAT(icing->PersistToDisk(PersistType::FULL).status(), ProtoIsOk());
+  PersistToDiskResultProto persist_result =
+      icing->PersistToDisk(PersistType::FULL);
+  ASSERT_THAT(persist_result.status(), ProtoIsOk());
 
-  OptimizeStatsProto expected;
-  expected.set_latency_ms(5);
-  expected.set_document_store_optimize_latency_ms(5);
-  expected.set_index_restoration_latency_ms(5);
-  expected.set_num_original_documents(3);
-  expected.set_num_deleted_documents(1);
-  expected.set_num_expired_documents(1);
-  expected.set_num_original_namespaces(1);
-  expected.set_num_deleted_namespaces(0);
-  expected.set_index_restoration_mode(OptimizeStatsProto::FULL_INDEX_REBUILD);
+  PersistToDiskStatsProto expected_persist_stats;
+  expected_persist_stats.set_persist_type(PersistType::FULL);
+  expected_persist_stats.set_latency_ms(5);
+  expected_persist_stats.set_schema_store_persist_latency_ms(5);
+  expected_persist_stats.set_document_store_total_persist_latency_ms(5);
+  expected_persist_stats.set_document_store_components_persist_latency_ms(5);
+  expected_persist_stats.set_document_store_checksum_update_latency_ms(5);
+  expected_persist_stats.set_document_log_checksum_update_latency_ms(5);
+  expected_persist_stats.set_document_log_data_sync_latency_ms(5);
+  expected_persist_stats.set_index_persist_latency_ms(5);
+  expected_persist_stats.set_integer_index_persist_latency_ms(5);
+  expected_persist_stats.set_qualified_id_join_index_persist_latency_ms(5);
+  expected_persist_stats.set_embedding_index_persist_latency_ms(5);
+  EXPECT_THAT(persist_result.persist_stats(),
+              EqualsProto(expected_persist_stats));
+
+  OptimizeStatsProto expected_optimize_stats;
+  expected_optimize_stats.set_latency_ms(5);
+  expected_optimize_stats.set_document_store_optimize_latency_ms(5);
+  expected_optimize_stats.set_index_restoration_latency_ms(5);
+  expected_optimize_stats.set_num_original_documents(3);
+  expected_optimize_stats.set_num_deleted_documents(1);
+  expected_optimize_stats.set_num_expired_documents(1);
+  expected_optimize_stats.set_num_original_namespaces(1);
+  expected_optimize_stats.set_num_deleted_namespaces(0);
+  expected_optimize_stats.set_index_restoration_mode(
+      OptimizeStatsProto::FULL_INDEX_REBUILD);
+
+  PersistToDiskStatsProto* expected_persist_stats_before_optimize =
+      expected_optimize_stats.mutable_before_optimize_persist_stats();
+  expected_persist_stats_before_optimize->set_persist_type(PersistType::FULL);
+  expected_persist_stats_before_optimize->set_latency_ms(5);
+  expected_persist_stats_before_optimize->set_schema_store_persist_latency_ms(
+      5);
+  expected_persist_stats_before_optimize
+      ->set_document_store_total_persist_latency_ms(5);
+  expected_persist_stats_before_optimize
+      ->set_document_store_components_persist_latency_ms(5);
+  expected_persist_stats_before_optimize
+      ->set_document_store_checksum_update_latency_ms(5);
+  expected_persist_stats_before_optimize->set_index_persist_latency_ms(5);
+  expected_persist_stats_before_optimize->set_integer_index_persist_latency_ms(
+      5);
+  expected_persist_stats_before_optimize
+      ->set_qualified_id_join_index_persist_latency_ms(5);
+  expected_persist_stats_before_optimize
+      ->set_embedding_index_persist_latency_ms(5);
+
+  PersistToDiskStatsProto* expected_persist_stats_after_optimize =
+      expected_optimize_stats.mutable_after_optimize_persist_stats();
+  expected_persist_stats_after_optimize->set_persist_type(PersistType::FULL);
+  expected_persist_stats_after_optimize->set_latency_ms(5);
+  expected_persist_stats_after_optimize->set_schema_store_persist_latency_ms(5);
+  expected_persist_stats_after_optimize
+      ->set_document_store_total_persist_latency_ms(5);
+  expected_persist_stats_after_optimize
+      ->set_document_store_components_persist_latency_ms(5);
+  expected_persist_stats_after_optimize
+      ->set_document_store_checksum_update_latency_ms(5);
+  expected_persist_stats_after_optimize->set_index_persist_latency_ms(5);
+  expected_persist_stats_after_optimize->set_integer_index_persist_latency_ms(
+      5);
+  expected_persist_stats_after_optimize
+      ->set_qualified_id_join_index_persist_latency_ms(5);
+  expected_persist_stats_after_optimize->set_embedding_index_persist_latency_ms(
+      5);
 
   // Run Optimize
   OptimizeResultProto result = icing->Optimize();
@@ -2084,7 +2333,7 @@
               Ge(result.optimize_stats().storage_size_after() - page_size));
   result.mutable_optimize_stats()->clear_storage_size_before();
   result.mutable_optimize_stats()->clear_storage_size_after();
-  EXPECT_THAT(result.optimize_stats(), EqualsProto(expected));
+  EXPECT_THAT(result.optimize_stats(), EqualsProto(expected_optimize_stats));
 
   fake_clock = std::make_unique<FakeClock>();
   fake_clock->SetTimerElapsedMilliseconds(5);
@@ -2097,18 +2346,58 @@
       GetTestJniCache());
   ASSERT_THAT(icing->Initialize().status(), ProtoIsOk());
 
-  expected = OptimizeStatsProto();
-  expected.set_latency_ms(5);
-  expected.set_document_store_optimize_latency_ms(5);
-  expected.set_index_restoration_latency_ms(5);
-  expected.set_num_original_documents(1);
-  expected.set_num_deleted_documents(0);
-  expected.set_num_expired_documents(0);
-  expected.set_num_original_namespaces(1);
-  expected.set_num_deleted_namespaces(0);
-  expected.set_time_since_last_optimize_ms(10000);
-  expected.set_time_since_last_successful_optimize_ms(10000);
-  expected.set_index_restoration_mode(OptimizeStatsProto::FULL_INDEX_REBUILD);
+  expected_optimize_stats = OptimizeStatsProto();
+  expected_optimize_stats.set_latency_ms(5);
+  expected_optimize_stats.set_document_store_optimize_latency_ms(5);
+  expected_optimize_stats.set_index_restoration_latency_ms(5);
+  expected_optimize_stats.set_num_original_documents(1);
+  expected_optimize_stats.set_num_deleted_documents(0);
+  expected_optimize_stats.set_num_expired_documents(0);
+  expected_optimize_stats.set_num_original_namespaces(1);
+  expected_optimize_stats.set_num_deleted_namespaces(0);
+  expected_optimize_stats.set_time_since_last_optimize_ms(10000);
+  expected_optimize_stats.set_time_since_last_successful_optimize_ms(10000);
+  expected_optimize_stats.set_index_restoration_mode(
+      OptimizeStatsProto::FULL_INDEX_REBUILD);
+
+  expected_persist_stats_before_optimize =
+      expected_optimize_stats.mutable_before_optimize_persist_stats();
+  expected_persist_stats_before_optimize->set_persist_type(PersistType::FULL);
+  expected_persist_stats_before_optimize->set_latency_ms(5);
+  expected_persist_stats_before_optimize->set_schema_store_persist_latency_ms(
+      5);
+  expected_persist_stats_before_optimize
+      ->set_document_store_total_persist_latency_ms(5);
+  expected_persist_stats_before_optimize
+      ->set_document_store_components_persist_latency_ms(5);
+  expected_persist_stats_before_optimize
+      ->set_document_store_checksum_update_latency_ms(5);
+  expected_persist_stats_before_optimize->set_index_persist_latency_ms(5);
+  expected_persist_stats_before_optimize->set_integer_index_persist_latency_ms(
+      5);
+  expected_persist_stats_before_optimize
+      ->set_qualified_id_join_index_persist_latency_ms(5);
+  expected_persist_stats_before_optimize
+      ->set_embedding_index_persist_latency_ms(5);
+
+  expected_persist_stats_after_optimize =
+      expected_optimize_stats.mutable_after_optimize_persist_stats();
+  expected_persist_stats_after_optimize->set_persist_type(PersistType::FULL);
+  expected_persist_stats_after_optimize->set_latency_ms(5);
+  expected_persist_stats_after_optimize->set_schema_store_persist_latency_ms(5);
+  expected_persist_stats_after_optimize
+      ->set_document_store_total_persist_latency_ms(5);
+  expected_persist_stats_after_optimize
+      ->set_document_store_components_persist_latency_ms(5);
+  expected_persist_stats_after_optimize
+      ->set_document_store_checksum_update_latency_ms(5);
+  expected_persist_stats_after_optimize->set_index_persist_latency_ms(5);
+  expected_persist_stats_after_optimize->set_integer_index_persist_latency_ms(
+      5);
+  expected_persist_stats_after_optimize
+      ->set_qualified_id_join_index_persist_latency_ms(5);
+  expected_persist_stats_after_optimize->set_embedding_index_persist_latency_ms(
+      5);
 
   // Run Optimize
   result = icing->Optimize();
@@ -2116,24 +2405,65 @@
               Eq(result.optimize_stats().storage_size_after()));
   result.mutable_optimize_stats()->clear_storage_size_before();
   result.mutable_optimize_stats()->clear_storage_size_after();
-  EXPECT_THAT(result.optimize_stats(), EqualsProto(expected));
+  EXPECT_THAT(result.optimize_stats(), EqualsProto(expected_optimize_stats));
 
   // Delete the last document.
   ASSERT_THAT(icing->Delete(document3.namespace_(), document3.uri()).status(),
               ProtoIsOk());
 
-  expected = OptimizeStatsProto();
-  expected.set_latency_ms(5);
-  expected.set_document_store_optimize_latency_ms(5);
-  expected.set_index_restoration_latency_ms(5);
-  expected.set_num_original_documents(1);
-  expected.set_num_deleted_documents(1);
-  expected.set_num_expired_documents(0);
-  expected.set_num_original_namespaces(1);
-  expected.set_num_deleted_namespaces(1);
-  expected.set_time_since_last_optimize_ms(0);
-  expected.set_time_since_last_successful_optimize_ms(0);
-  expected.set_index_restoration_mode(OptimizeStatsProto::FULL_INDEX_REBUILD);
+  expected_optimize_stats = OptimizeStatsProto();
+  expected_optimize_stats.set_latency_ms(5);
+  expected_optimize_stats.set_document_store_optimize_latency_ms(5);
+  expected_optimize_stats.set_index_restoration_latency_ms(5);
+  expected_optimize_stats.set_num_original_documents(1);
+  expected_optimize_stats.set_num_deleted_documents(1);
+  expected_optimize_stats.set_num_expired_documents(0);
+  expected_optimize_stats.set_num_original_namespaces(1);
+  expected_optimize_stats.set_num_deleted_namespaces(1);
+  expected_optimize_stats.set_time_since_last_optimize_ms(0);
+  expected_optimize_stats.set_time_since_last_successful_optimize_ms(0);
+  expected_optimize_stats.set_index_restoration_mode(
+      OptimizeStatsProto::FULL_INDEX_REBUILD);
+
+  expected_persist_stats_before_optimize =
+      expected_optimize_stats.mutable_before_optimize_persist_stats();
+  expected_persist_stats_before_optimize->set_persist_type(PersistType::FULL);
+  expected_persist_stats_before_optimize->set_latency_ms(5);
+  expected_persist_stats_before_optimize->set_schema_store_persist_latency_ms(
+      5);
+  expected_persist_stats_before_optimize
+      ->set_document_store_total_persist_latency_ms(5);
+
+  expected_persist_stats_before_optimize
+      ->set_document_store_components_persist_latency_ms(5);
+  expected_persist_stats_before_optimize
+      ->set_document_store_checksum_update_latency_ms(5);
+  expected_persist_stats_before_optimize->set_index_persist_latency_ms(5);
+  expected_persist_stats_before_optimize->set_integer_index_persist_latency_ms(
+      5);
+  expected_persist_stats_before_optimize
+      ->set_qualified_id_join_index_persist_latency_ms(5);
+  expected_persist_stats_before_optimize
+      ->set_embedding_index_persist_latency_ms(5);
+
+  expected_persist_stats_after_optimize =
+      expected_optimize_stats.mutable_after_optimize_persist_stats();
+  expected_persist_stats_after_optimize->set_persist_type(PersistType::FULL);
+  expected_persist_stats_after_optimize->set_latency_ms(5);
+  expected_persist_stats_after_optimize->set_schema_store_persist_latency_ms(5);
+  expected_persist_stats_after_optimize
+      ->set_document_store_total_persist_latency_ms(5);
+  expected_persist_stats_after_optimize
+      ->set_document_store_components_persist_latency_ms(5);
+  expected_persist_stats_after_optimize
+      ->set_document_store_checksum_update_latency_ms(5);
+  expected_persist_stats_after_optimize->set_index_persist_latency_ms(5);
+  expected_persist_stats_after_optimize->set_integer_index_persist_latency_ms(
+      5);
+  expected_persist_stats_after_optimize
+      ->set_qualified_id_join_index_persist_latency_ms(5);
+  expected_persist_stats_after_optimize->set_embedding_index_persist_latency_ms(
+      5);
 
   // Run Optimize
   result = icing->Optimize();
@@ -2141,7 +2471,7 @@
               Ge(result.optimize_stats().storage_size_after()));
   result.mutable_optimize_stats()->clear_storage_size_before();
   result.mutable_optimize_stats()->clear_storage_size_after();
-  EXPECT_THAT(result.optimize_stats(), EqualsProto(expected));
+  EXPECT_THAT(result.optimize_stats(), EqualsProto(expected_optimize_stats));
 }
 
 TEST_F(IcingSearchEngineOptimizeTest,
@@ -2170,6 +2500,7 @@
           .Build();
   IcingSearchEngineOptions icing_options = GetDefaultIcingOptions();
   icing_options.set_compression_level(3);
+  icing_options.set_compression_threshold_bytes(0);
   int64_t document_log_size_compression_3;
   int64_t document_log_size_after_opti_no_compression;
   int64_t document_log_size_after_opti_compression_3;
diff --git a/icing/icing-search-engine_put_test.cc b/icing/icing-search-engine_put_test.cc
index 37df1a1..ad76b98 100644
--- a/icing/icing-search-engine_put_test.cc
+++ b/icing/icing-search-engine_put_test.cc
@@ -12,10 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include <chrono>  // NOLINT
 #include <cstdint>
 #include <limits>
 #include <memory>
 #include <string>
+#include <thread>  // NOLINT
 #include <utility>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
@@ -45,6 +47,7 @@
 #include "icing/proto/term.pb.h"
 #include "icing/proto/usage.pb.h"
 #include "icing/schema-builder.h"
+#include "icing/store/document-log-creator.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
 #include "icing/testing/jni-test-helpers.h"
@@ -136,6 +139,10 @@
 IcingSearchEngineOptions GetDefaultIcingOptions() {
   IcingSearchEngineOptions icing_options;
   icing_options.set_base_dir(GetTestBaseDir());
+  icing_options.set_enable_repeated_field_joins(true);
+  icing_options.set_enable_soft_index_restoration(true);
+  icing_options.set_enable_qualified_id_join_index_v3(true);
+  icing_options.set_enable_delete_propagation_from(true);
   return icing_options;
 }
 
@@ -602,6 +609,1108 @@
               ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
 }
 
+TEST_F(IcingSearchEnginePutTest, DocumentCompressionThreshold) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Message").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("body")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+
+  // Create document1 with size of 1024 bytes.
+  int64_t document1_size = 1024;
+  DocumentProto document1 =
+      DocumentBuilder()
+          .SetKey("namespace", "uri1")
+          .SetSchema("Message")
+          .AddStringProperty("body", std::string(979, 'a'))
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  ASSERT_THAT(document1.ByteSizeLong(), Eq(document1_size));
+
+  // Create document2 with size of 900 bytes.
+  int64_t document2_size = 900;
+  DocumentProto document2 =
+      DocumentBuilder()
+          .SetKey("namespace", "uri2")
+          .SetSchema("Message")
+          .AddStringProperty("body", std::string(855, 'a'))
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  ASSERT_THAT(document2.ByteSizeLong(), Eq(document2_size));
+
+  // Create icing instance with compression threshold of 1000 bytes.
+  IcingSearchEngineOptions icing_options = GetDefaultIcingOptions();
+  icing_options.set_compression_level(3);
+
+  int64_t document_log_size_full_compression;
+  int64_t document_log_size_part_compression;
+  int64_t document_log_size_no_compression;
+  const std::string document_log_path =
+      icing_options.base_dir() + "/document_dir/" +
+      DocumentLogCreator::GetDocumentLogFilename();
+
+  // Set the compression threshold to 1000 bytes to only compress document1.
+  {
+    icing_options.set_compression_threshold_bytes(1000);
+    IcingSearchEngine icing(icing_options, GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk());
+    ASSERT_THAT(icing.PersistToDisk(PersistType::FULL).status(), ProtoIsOk());
+
+    document_log_size_part_compression =
+        filesystem()->GetFileSize(document_log_path.c_str());
+
+    // Calling Optimize() will not change the size of the document log since
+    // the compression setting is the same.
+    ASSERT_THAT(icing.Optimize().status(), ProtoIsOk());
+    ASSERT_EQ(filesystem()->GetFileSize(document_log_path.c_str()),
+              document_log_size_part_compression);
+  }
+
+  // Set the compression threshold to 900 bytes to compress both documents.
+  {
+    icing_options.set_compression_threshold_bytes(900);
+    IcingSearchEngine icing(icing_options, GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+    // Call Optimize() to refresh the document log with the new compression
+    // threshold.
+    ASSERT_THAT(icing.Optimize().status(), ProtoIsOk());
+    document_log_size_full_compression =
+        filesystem()->GetFileSize(document_log_path.c_str());
+
+    // Calling Optimize() again will not change the size of the document log
+    // since the compression setting is the same.
+    ASSERT_THAT(icing.Optimize().status(), ProtoIsOk());
+    ASSERT_EQ(filesystem()->GetFileSize(document_log_path.c_str()),
+              document_log_size_full_compression);
+  }
+
+  // Set the compression threshold to 10000 bytes will turn off compression.
+  {
+    icing_options.set_compression_threshold_bytes(10000);
+    IcingSearchEngine icing(icing_options, GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+    // Call Optimize() to refresh the document log with the new compression
+    // threshold.
+    ASSERT_THAT(icing.Optimize().status(), ProtoIsOk());
+    document_log_size_no_compression =
+        filesystem()->GetFileSize(document_log_path.c_str());
+
+    // Calling Optimize() again will not change the size of the document log
+    // since the compression setting is the same.
+    ASSERT_THAT(icing.Optimize().status(), ProtoIsOk());
+    ASSERT_EQ(filesystem()->GetFileSize(document_log_path.c_str()),
+              document_log_size_no_compression);
+  }
+
+  // Check that no_compression > part_compression > full_compression
+  ASSERT_GT(document_log_size_part_compression,
+            document_log_size_full_compression);
+  ASSERT_GT(document_log_size_no_compression,
+            document_log_size_part_compression);
+}
+
+TEST_F(
+    IcingSearchEnginePutTest,
+    PutDocument_taskSchedulerDisabled_shouldNotReschedulePurgingExpirationTask) {
+  // This test verifies that when enable_delete_propagation_from is true but
+  // enable_background_task_scheduler is false, then:
+  // - No background tasks are scheduled.
+  // - Since task_scheduler_ is null, APIs attempt to (re)schedule a task should
+  //   check the nullness of task_scheduler_ before using it.
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto person = DocumentBuilder()
+                             .SetKey("namespace", "person")
+                             .SetSchema("Person")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(1000)  // Expire at 1010 ms.
+                             .AddStringProperty("name", "Alice")
+                             .Build();
+  DocumentProto email = DocumentBuilder()
+                            .SetKey("namespace", "email")
+                            .SetSchema("Email")
+                            .SetCreationTimestampMs(10)
+                            .SetTtlMs(0)  // Never expire.
+                            .AddStringProperty("subject", "test")
+                            .AddStringProperty("sender", "namespace#person")
+                            .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_background_task_scheduler(false);
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  // Initialize Icing with a fake clock and t = 10 ms.
+  auto fake_clock = std::make_unique<FakeClock>();
+  FakeClock* fake_clock_ptr = fake_clock.get();
+  fake_clock->SetSystemTimeMilliseconds(10);
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+
+  // Put person and email. Since enable_background_task_scheduler is false,
+  // there should be no background tasks scheduled.
+  ASSERT_THAT(icing.Put(person).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email).status(), ProtoIsOk());
+
+  // Sanity check that person and email are present.
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto1.mutable_document() = person;
+  EXPECT_THAT(
+      icing.Get("namespace", "person", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto1));
+
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_google::protobuf.mutable_document() = email;
+  EXPECT_THAT(
+      icing.Get("namespace", "email", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_google::protobuf));
+
+  // Adjust the clock to 1010 ms and sleep for 1010 ms. Email document should
+  // still be present since no background tasks were and therefore, expiration
+  // propagation and purging did not run for the child document.
+  fake_clock_ptr->SetSystemTimeMilliseconds(1010);
+  std::this_thread::sleep_for(std::chrono::milliseconds(1010));
+
+  GetResultProto expected_get_result_proto4;
+  expected_get_result_proto4.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto4.mutable_document() = email;
+  EXPECT_THAT(
+      icing.Get("namespace", "email", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto4));
+}
+
+TEST_F(IcingSearchEnginePutTest,
+       PutDocument_shouldReschedulePurgingExpirationTaskForNewExpTs) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto person = DocumentBuilder()
+                             .SetKey("namespace", "person")
+                             .SetSchema("Person")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(1000)  // Expire at 1010 ms.
+                             .AddStringProperty("name", "Alice")
+                             .Build();
+  DocumentProto email = DocumentBuilder()
+                            .SetKey("namespace", "email")
+                            .SetSchema("Email")
+                            .SetCreationTimestampMs(10)
+                            .SetTtlMs(0)  // Never expire.
+                            .AddStringProperty("subject", "test")
+                            .AddStringProperty("sender", "namespace#person")
+                            .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_background_task_scheduler(true);
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  // Initialize Icing with a fake clock and t = 10 ms. At this moment, there is
+  // no expiration document so the purging expired document task is not
+  // scheduled.
+  auto fake_clock = std::make_unique<FakeClock>();
+  FakeClock* fake_clock_ptr = fake_clock.get();
+  fake_clock->SetSystemTimeMilliseconds(10);
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+
+  // Put person and email. The scheduler should schedule the purging expiration
+  // task at t = 1010 ms.
+  ASSERT_THAT(icing.Put(person).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email).status(), ProtoIsOk());
+
+  // Sanity check that person and email are present.
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto1.mutable_document() = person;
+  EXPECT_THAT(
+      icing.Get("namespace", "person", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto1));
+
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_google::protobuf.mutable_document() = email;
+  EXPECT_THAT(
+      icing.Get("namespace", "email", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_google::protobuf));
+
+  // Adjust the clock to 1010 ms and sleep for 1010 ms. The purging expiration
+  // task should execute and purge person and email.
+  // - At t = 1010 ms, person is expired, but DocumentFilterData has already
+  //   filtered it out, so we're unsure whether it is purged or not.
+  // - But the child document email never expires and DocumentFilterData
+  //   doesn't filter it out. If the scheduled task had not worked correctly,
+  //   then expiration propagation would have not been triggered and email
+  //   would've been alive. Therefore, we can verify it by checking email.
+  fake_clock_ptr->SetSystemTimeMilliseconds(1010);
+  std::this_thread::sleep_for(std::chrono::milliseconds(1010));
+
+  GetResultProto expected_get_result_proto3;
+  expected_get_result_proto3.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto3.mutable_status()->set_message(
+      "Document (namespace, person) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "person", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto3));
+
+  GetResultProto expected_get_result_proto4;
+  expected_get_result_proto4.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto4.mutable_status()->set_message(
+      "Document (namespace, email) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "email", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto4));
+}
+
+TEST_F(IcingSearchEnginePutTest,
+       PutDocument_shouldReschedulePurgingExpirationTaskForSmallerExpTs) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto person1 = DocumentBuilder()
+                              .SetKey("namespace", "person1")
+                              .SetSchema("Person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(2000000)  // Expire at 2000010 ms.
+                              .AddStringProperty("name", "Alice")
+                              .Build();
+  DocumentProto person2 = DocumentBuilder()
+                              .SetKey("namespace", "person2")
+                              .SetSchema("Person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(1000)  // Expire at 1010 ms.
+                              .AddStringProperty("name", "Bob")
+                              .Build();
+  DocumentProto email1 = DocumentBuilder()
+                             .SetKey("namespace", "email1")
+                             .SetSchema("Email")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("subject", "test")
+                             .AddStringProperty("sender", "namespace#person1")
+                             .Build();
+  DocumentProto email2 = DocumentBuilder()
+                             .SetKey("namespace", "email2")
+                             .SetSchema("Email")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("subject", "test")
+                             .AddStringProperty("sender", "namespace#person2")
+                             .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_background_task_scheduler(true);
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  {
+    // Initialize Icing and put person1 and email1. Destruct Icing.
+    TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                                std::make_unique<IcingFilesystem>(),
+                                std::make_unique<FakeClock>(),
+                                GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(person1).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(email1).status(), ProtoIsOk());
+  }
+
+  // Initialize Icing again with a fake clock and t = 10 ms. Initialization
+  // should schedule the purging expiration task at t = 2000010 ms.
+  auto fake_clock = std::make_unique<FakeClock>();
+  FakeClock* fake_clock_ptr = fake_clock.get();
+  fake_clock->SetSystemTimeMilliseconds(10);
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  // Put person2 and email2. It should reschedule the purging expiration task at
+  // t = 1010 ms.
+  ASSERT_THAT(icing.Put(person2).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email2).status(), ProtoIsOk());
+
+  // Sanity check that person2 and email2 are present.
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto1.mutable_document() = person2;
+  EXPECT_THAT(
+      icing.Get("namespace", "person2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto1));
+
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_google::protobuf.mutable_document() = email2;
+  EXPECT_THAT(
+      icing.Get("namespace", "email2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_google::protobuf));
+
+  // Adjust the clock to 1010 ms and sleep for 1010 ms. The purging expiration
+  // task should execute and purge person2 and email2.
+  // - At t = 1010 ms, person2 is expired, but DocumentFilterData has already
+  //   filtered it out, so we're unsure whether it is purged or not.
+  // - But the child document email2 never expires and DocumentFilterData
+  //   doesn't filter it out. If the scheduled task had not worked correctly,
+  //   then expiration propagation would have not been triggered and email2
+  //   would've been alive. Therefore, we can verify it by checking email2.
+  fake_clock_ptr->SetSystemTimeMilliseconds(1010);
+  std::this_thread::sleep_for(std::chrono::milliseconds(1010));
+
+  GetResultProto expected_get_result_proto3;
+  expected_get_result_proto3.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto3.mutable_status()->set_message(
+      "Document (namespace, person2) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "person2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto3));
+
+  GetResultProto expected_get_result_proto4;
+  expected_get_result_proto4.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto4.mutable_status()->set_message(
+      "Document (namespace, email2) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "email2", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto4));
+}
+
+TEST_F(IcingSearchEnginePutTest,
+       PutDocument_shouldNotReschedulePurgingExpirationTaskForGreaterExpTs) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto person1 = DocumentBuilder()
+                              .SetKey("namespace", "person1")
+                              .SetSchema("Person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(10000)  // Expire at 10010 ms.
+                              .AddStringProperty("name", "Alice")
+                              .Build();
+  DocumentProto person2 = DocumentBuilder()
+                              .SetKey("namespace", "person2")
+                              .SetSchema("Person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(2000000)  // Expire at 2000010 ms.
+                              .AddStringProperty("name", "Bob")
+                              .Build();
+  DocumentProto email1 = DocumentBuilder()
+                             .SetKey("namespace", "email1")
+                             .SetSchema("Email")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("subject", "test")
+                             .AddStringProperty("sender", "namespace#person1")
+                             .Build();
+  DocumentProto email2 = DocumentBuilder()
+                             .SetKey("namespace", "email2")
+                             .SetSchema("Email")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("subject", "test")
+                             .AddStringProperty("sender", "namespace#person2")
+                             .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_background_task_scheduler(true);
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  {
+    // Initialize Icing and put person1 and email1. Destruct Icing.
+    TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                                std::make_unique<IcingFilesystem>(),
+                                std::make_unique<FakeClock>(),
+                                GetTestJniCache());
+    ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(person1).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(email1).status(), ProtoIsOk());
+  }
+
+  // Initialize Icing again with a fake clock and t = 9010 ms. Initialization
+  // should schedule the purging expiration task at t = 10010 ms.
+  auto fake_clock = std::make_unique<FakeClock>();
+  FakeClock* fake_clock_ptr = fake_clock.get();
+  fake_clock->SetSystemTimeMilliseconds(9010);
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  // Put person2 and email2. It should NOT reschedule the purging expiration
+  // task since the new expiration timestamp is greater than the current
+  // scheduled one.
+  ASSERT_THAT(icing.Put(person2).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email2).status(), ProtoIsOk());
+
+  // Sanity check that person1 and email1 are present.
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto1.mutable_document() = person1;
+  EXPECT_THAT(
+      icing.Get("namespace", "person1", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto1));
+
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_google::protobuf.mutable_document() = email1;
+  EXPECT_THAT(
+      icing.Get("namespace", "email1", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_google::protobuf));
+
+  // Adjust the clock to 10010 ms and sleep for 1100 ms. The purging expiration
+  // task should execute and purge person1 and email1.
+  // - If Put API had falsely rescheduled the task, then at t = 10010 ms person1
+  //   and email1 would've been alive. We need to verify that the task was not
+  //   rescheduled with the greater expiration timestamp.
+  // - At t = 10010 ms, person1 is expired, but DocumentFilterData has already
+  //   filtered it out, so we're unsure whether it is purged or not.
+  // - But the child document email1 never expires and DocumentFilterData
+  //   doesn't filter it out. If the scheduled task had not worked correctly,
+  //   then expiration propagation would have not been triggered and email1
+  //   would've been alive. Therefore, we can verify it by checking email1.
+  fake_clock_ptr->SetSystemTimeMilliseconds(10010);
+  std::this_thread::sleep_for(std::chrono::milliseconds(1100));
+
+  GetResultProto expected_get_result_proto3;
+  expected_get_result_proto3.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto3.mutable_status()->set_message(
+      "Document (namespace, person1) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "person1", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto3));
+
+  GetResultProto expected_get_result_proto4;
+  expected_get_result_proto4.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto4.mutable_status()->set_message(
+      "Document (namespace, email1) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "email1", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto4));
+}
+
+TEST_F(IcingSearchEnginePutTest,
+       PutDocument_shouldSucceedWithDeletePropagationEnabledAndValidParent) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto person = DocumentBuilder()
+                             .SetKey("namespace", "person")
+                             .SetSchema("Person")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(10000)  // Expire at 10010 ms.
+                             .AddStringProperty("name", "Alice")
+                             .Build();
+  DocumentProto email = DocumentBuilder()
+                            .SetKey("namespace", "email")
+                            .SetSchema("Email")
+                            .SetCreationTimestampMs(10)
+                            .SetTtlMs(0)  // Never expire.
+                            .AddStringProperty("subject", "test")
+                            .AddStringProperty("sender", "namespace#person")
+                            .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  // Initialize Icing.
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetSystemTimeMilliseconds(10);
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(person).status(), ProtoIsOk());
+
+  // Put email document should succeed.
+  EXPECT_THAT(icing.Put(email).status(), ProtoIsOk());
+
+  // Can get email document.
+  GetResultProto expected_get_result_proto;
+  expected_get_result_proto.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto.mutable_document() = email;
+  EXPECT_THAT(
+      icing.Get("namespace", "email", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto));
+}
+
+TEST_F(
+    IcingSearchEnginePutTest,
+    PutDocument_shouldSucceedWithDeletePropagationEnabledAndEmptyQualifiedId) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto email =
+      DocumentBuilder()
+          .SetKey("namespace", "email")
+          .SetSchema("Email")
+          .SetCreationTimestampMs(10)
+          .SetTtlMs(0)  // Never expire.
+          .AddStringProperty("subject", "test")
+          .AddStringProperty("sender", "")  // Empty qualified id.
+          .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  // Initialize Icing.
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetSystemTimeMilliseconds(10);
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+
+  // Put email document should succeed.
+  EXPECT_THAT(icing.Put(email).status(), ProtoIsOk());
+
+  // Can get email document.
+  GetResultProto expected_get_result_proto;
+  expected_get_result_proto.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto.mutable_document() = email;
+  EXPECT_THAT(
+      icing.Get("namespace", "email", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto));
+}
+
+TEST_F(
+    IcingSearchEnginePutTest,
+    PutDocument_shouldFailWithDeletePropagationEnabledAndInvalidQualifiedId) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto email = DocumentBuilder()
+                            .SetKey("namespace", "email")
+                            .SetSchema("Email")
+                            .SetCreationTimestampMs(10)
+                            .SetTtlMs(0)  // Never expire.
+                            .AddStringProperty("subject", "test")
+                            .AddStringProperty("sender", "InvalidQualifiedId")
+                            .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  // Initialize Icing.
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetSystemTimeMilliseconds(10);
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+
+  // Put email document should fail.
+  EXPECT_THAT(icing.Put(email).status(),
+              ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
+}
+
+TEST_F(IcingSearchEnginePutTest,
+       PutDocument_shouldFailWithDeletePropagationEnabledAndNonExistentParent) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto email =
+      DocumentBuilder()
+          .SetKey("namespace", "email")
+          .SetSchema("Email")
+          .SetCreationTimestampMs(10)
+          .SetTtlMs(0)  // Never expire.
+          .AddStringProperty("subject", "test")
+          .AddStringProperty("sender",
+                             "namespace#person")  // Non-existent parent.
+          .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  // Initialize Icing.
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetSystemTimeMilliseconds(10);
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+
+  // Put email document should fail.
+  EXPECT_THAT(icing.Put(email).status(),
+              ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
+}
+
+TEST_F(IcingSearchEnginePutTest,
+       PutDocument_shouldFailWithDeletePropagationEnabledAndExpiredParent) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto person = DocumentBuilder()
+                             .SetKey("namespace", "person")
+                             .SetSchema("Person")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(10000)  // Expire at 10010 ms.
+                             .AddStringProperty("name", "Alice")
+                             .Build();
+  DocumentProto email = DocumentBuilder()
+                            .SetKey("namespace", "email")
+                            .SetSchema("Email")
+                            .SetCreationTimestampMs(10)
+                            .SetTtlMs(0)  // Never expire.
+                            .AddStringProperty("subject", "test")
+                            .AddStringProperty("sender", "namespace#person")
+                            .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  // Initialize Icing.
+  auto fake_clock = std::make_unique<FakeClock>();
+  FakeClock* fake_clock_ptr = fake_clock.get();
+  fake_clock->SetSystemTimeMilliseconds(10);
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(person).status(), ProtoIsOk());
+
+  // Adjust the clock to 20000 ms and put email document. Since person document
+  // is expired, put email document should fail.
+  fake_clock_ptr->SetSystemTimeMilliseconds(20000);
+  EXPECT_THAT(icing.Put(email).status(),
+              ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
+}
+
+TEST_F(IcingSearchEnginePutTest,
+       PutDocument_shouldFailWithDeletePropagationEnabledAndDeletedParent) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("sender")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto person = DocumentBuilder()
+                             .SetKey("namespace", "person")
+                             .SetSchema("Person")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("name", "Alice")
+                             .Build();
+  DocumentProto email = DocumentBuilder()
+                            .SetKey("namespace", "email")
+                            .SetSchema("Email")
+                            .SetCreationTimestampMs(10)
+                            .SetTtlMs(0)  // Never expire.
+                            .AddStringProperty("subject", "test")
+                            .AddStringProperty("sender", "namespace#person")
+                            .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  // Initialize Icing.
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetSystemTimeMilliseconds(10);
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(person).status(), ProtoIsOk());
+
+  // Delete person document. Put email document should fail afterwards.
+  ASSERT_THAT(icing.Delete("namespace", "person").status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(email).status(),
+              ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
+}
+
+TEST_F(
+    IcingSearchEnginePutTest,
+    PutDocument_shouldSucceedWithDeletePropagationDisabledAndNonAliveParents) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Email")
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("subject")
+                                   .SetDataTypeString(TERM_MATCH_PREFIX,
+                                                      TOKENIZER_PLAIN)
+                                   .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("sender")
+                          .SetDataTypeJoinableString(
+                              JOINABLE_VALUE_TYPE_QUALIFIED_ID)  // No delete
+                                                                 // propagation.
+                          .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto person1 = DocumentBuilder()
+                              .SetKey("namespace", "person/1")
+                              .SetSchema("Person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(10000)  // Expire at 10010 ms.
+                              .AddStringProperty("name", "Alice")
+                              .Build();
+  DocumentProto person2 = DocumentBuilder()
+                              .SetKey("namespace", "person/2")
+                              .SetSchema("Person")
+                              .SetCreationTimestampMs(10)
+                              .SetTtlMs(0)  // Never expire.
+                              .AddStringProperty("name", "Bob")
+                              .Build();
+  DocumentProto email1 = DocumentBuilder()
+                             .SetKey("namespace", "email/1")
+                             .SetSchema("Email")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("subject", "test")
+                             .AddStringProperty("sender", "InvalidQualifiedId")
+                             .Build();
+  DocumentProto email2 =
+      DocumentBuilder()
+          .SetKey("namespace", "email/2")
+          .SetSchema("Email")
+          .SetCreationTimestampMs(10)
+          .SetTtlMs(0)  // Never expire.
+          .AddStringProperty("subject", "test")
+          .AddStringProperty(
+              "sender", "namespace#NonExistentParent")  // Non-existent parent.
+          .Build();
+  DocumentProto email3 = DocumentBuilder()
+                             .SetKey("namespace", "email/3")
+                             .SetSchema("Email")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("subject", "test")
+                             .AddStringProperty("sender", "namespace#person/1")
+                             .Build();
+  DocumentProto email4 = DocumentBuilder()
+                             .SetKey("namespace", "email/4")
+                             .SetSchema("Email")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(0)  // Never expire.
+                             .AddStringProperty("subject", "test")
+                             .AddStringProperty("sender", "namespace#person/2")
+                             .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  // Initialize Icing.
+  auto fake_clock = std::make_unique<FakeClock>();
+  FakeClock* fake_clock_ptr = fake_clock.get();
+  fake_clock->SetSystemTimeMilliseconds(10);
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(person1).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(person2).status(), ProtoIsOk());
+
+  // Put email1, email2 should succeed, even though they contain invalid
+  // qualified id and non-existent parent respectively.
+  EXPECT_THAT(icing.Put(email1).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(email2).status(), ProtoIsOk());
+
+  // Adjust the clock to 20000 ms and put email3. Although person1 is expired
+  // and email3 contains a joinable qualified id to person1, putting email3
+  // should succeed since delete propagation is disabled.
+  fake_clock_ptr->SetSystemTimeMilliseconds(20000);
+  EXPECT_THAT(icing.Put(email3).status(), ProtoIsOk());
+
+  // Delete person2. Although email4 contains a joinable qualified id to
+  // person2, putting email4 should succeed since delete propagation is
+  // disabled.
+  ASSERT_THAT(icing.Delete("namespace", "person/2").status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(email4).status(), ProtoIsOk());
+}
+
+TEST_F(IcingSearchEnginePutTest,
+       PutDocument_shouldSetExpirationTimestampInPutResultProto) {
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetSystemTimeMilliseconds(10);
+  TestIcingSearchEngine icing(GetDefaultIcingOptions(),
+                              std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("icing", "message/1")
+                                .SetSchema("Message")
+                                .SetCreationTimestampMs(10)
+                                .SetTtlMs(10000)  // Expire at 10010 ms.
+                                .AddStringProperty("body", "message body")
+                                .Build();
+  PutResultProto put_result_proto1 = icing.Put(document1);
+  ASSERT_THAT(put_result_proto1.status(), ProtoIsOk());
+  EXPECT_THAT(put_result_proto1.document_expiration_timestamp_ms(), Eq(10010));
+
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("icing", "message/2")
+                                .SetSchema("Message")
+                                .SetCreationTimestampMs(10)
+                                .SetTtlMs(0)  // Never expire.
+                                .AddStringProperty("body", "message body")
+                                .Build();
+  PutResultProto put_result_google::protobuf = icing.Put(document2);
+  ASSERT_THAT(put_result_google::protobuf.status(), ProtoIsOk());
+  EXPECT_THAT(put_result_google::protobuf.document_expiration_timestamp_ms(),
+              Eq(std::numeric_limits<int64_t>::max()));
+}
+
 }  // namespace
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/icing-search-engine_schema_test.cc b/icing/icing-search-engine_schema_test.cc
index 269daaa..735d8d6 100644
--- a/icing/icing-search-engine_schema_test.cc
+++ b/icing/icing-search-engine_schema_test.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include <algorithm>
 #include <cstdint>
 #include <limits>
 #include <memory>
@@ -132,6 +133,7 @@
   icing_options.set_enable_schema_database(true);
   icing_options.set_enable_qualified_id_join_index_v3(true);
   icing_options.set_enable_delete_propagation_from(false);
+  icing_options.set_enable_scorable_properties(true);
   return icing_options;
 }
 
@@ -411,10 +413,13 @@
 
     SetSchemaResultProto set_schema_result = icing.SetSchema(schema);
     // Ignore latency numbers. They're covered elsewhere.
-    set_schema_result.clear_latency_ms();
+    set_schema_result.clear_set_schema_stats();
+    set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
     SetSchemaResultProto expected_set_schema_result;
     expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
     expected_set_schema_result.mutable_new_schema_types()->Add("Email");
+    expected_set_schema_result.set_needs_persist_type(
+        PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
     EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
     EXPECT_THAT(icing.GetSchema().schema().types(0).version(), Eq(1));
@@ -441,11 +446,14 @@
     // 3. SetSchema should succeed and the version number should be updated.
     SetSchemaResultProto set_schema_result = icing.SetSchema(schema, true);
     // Ignore latency numbers. They're covered elsewhere.
-    set_schema_result.clear_latency_ms();
+    set_schema_result.clear_set_schema_stats();
+    set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
     SetSchemaResultProto expected_set_schema_result;
     expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
     expected_set_schema_result.mutable_fully_compatible_changed_schema_types()
         ->Add("Email");
+    expected_set_schema_result.set_needs_persist_type(
+        PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
     EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
     EXPECT_THAT(icing.GetSchema().schema().types(0).version(), Eq(2));
@@ -665,12 +673,20 @@
   SetSchemaResultProto set_schema_result = icing.SetSchema(invalid_schema);
   EXPECT_THAT(set_schema_result.status(),
               ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
-  EXPECT_THAT(set_schema_result.latency_ms(), Eq(1000));
+  EXPECT_THAT(set_schema_result.set_schema_stats().overall_latency_ms(),
+              Eq(1000));
 
   // Can add an document of a set schema
   set_schema_result = icing.SetSchema(schema_with_message);
   EXPECT_THAT(set_schema_result.status(), ProtoStatusIs(StatusProto::OK));
-  EXPECT_THAT(set_schema_result.latency_ms(), Eq(1000));
+  EXPECT_THAT(set_schema_result.set_schema_stats().overall_latency_ms(),
+              Eq(1000));
+  EXPECT_THAT(
+      set_schema_result.set_schema_stats().schema_store_set_schema_latency_ms(),
+      Eq(1000));
+  EXPECT_THAT(set_schema_result.needs_persist_type(),
+              Eq(PersistType::UNKNOWN));  // Flush is not needed for the first
+                                          // SetSchema.
   EXPECT_THAT(icing.Put(message_document).status(), ProtoIsOk());
 
   // Schema with Email doesn't have Message, so would result incompatible
@@ -678,13 +694,21 @@
   set_schema_result = icing.SetSchema(schema_with_email);
   EXPECT_THAT(set_schema_result.status(),
               ProtoStatusIs(StatusProto::FAILED_PRECONDITION));
-  EXPECT_THAT(set_schema_result.latency_ms(), Eq(1000));
+  EXPECT_THAT(set_schema_result.set_schema_stats().overall_latency_ms(),
+              Eq(1000));
+  EXPECT_THAT(
+      set_schema_result.set_schema_stats().schema_store_set_schema_latency_ms(),
+      Eq(1000));
 
-  // Can expand the set of schema types and add an document of a new
-  // schema type
+  // Can expand the set of schema types and add an document of a new schema
+  // type.
   set_schema_result = icing.SetSchema(schema_with_email_and_message);
   EXPECT_THAT(set_schema_result.status(), ProtoStatusIs(StatusProto::OK));
-  EXPECT_THAT(set_schema_result.latency_ms(), Eq(1000));
+  EXPECT_THAT(set_schema_result.set_schema_stats().overall_latency_ms(),
+              Eq(1000));
+  EXPECT_THAT(
+      set_schema_result.set_schema_stats().schema_store_set_schema_latency_ms(),
+      Eq(1000));
 
   EXPECT_THAT(icing.Put(message_document).status(), ProtoIsOk());
   // Can't add an document whose schema isn't set
@@ -702,14 +726,84 @@
 TEST_F(IcingSearchEngineSchemaTest, SetSchema_schemaTypeIdChanged) {
   auto fake_clock = std::make_unique<FakeClock>();
   fake_clock->SetTimerElapsedMilliseconds(1000);
-  TestIcingSearchEngine icing(GetDefaultIcingOptions(),
-                              std::make_unique<Filesystem>(),
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
                               std::make_unique<IcingFilesystem>(),
                               std::move(fake_clock), GetTestJniCache());
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
 
+  // We need to add 2 dummy types to the schema so that once deleted, both
+  // Person and Email types' schema type ids will change.
   SchemaProto schema_one =
       SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder().SetType("DummyA"))  // schema type id 0
+          .AddType(
+              SchemaTypeConfigBuilder().SetType("DummyB"))  // schema type id 1
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Person")  // schema type id 2
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("name")
+                                        .SetDataTypeString(TERM_MATCH_PREFIX,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("age")
+                                        .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Email")  // schema type id 3
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("subject")
+                                        .SetDataTypeString(TERM_MATCH_PREFIX,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("sender")
+                                        .SetDataTypeJoinableString(
+                                            JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                        .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+
+  // Set schema with Person and Email types.
+  SetSchemaResultProto set_schema_result1 = icing.SetSchema(schema_one);
+  EXPECT_THAT(set_schema_result1.status(), ProtoStatusIs(StatusProto::OK));
+  EXPECT_THAT(set_schema_result1.set_schema_stats().overall_latency_ms(),
+              Eq(1000));
+  EXPECT_THAT(set_schema_result1.set_schema_stats()
+                  .schema_store_set_schema_latency_ms(),
+              Eq(1000));
+  EXPECT_THAT(set_schema_result1.needs_persist_type(),
+              Eq(PersistType::UNKNOWN));  // Flush is not needed for the first
+                                          // SetSchema.
+
+  // Put a person document and an email document.
+  DocumentProto person_document = DocumentBuilder()
+                                      .SetKey("namespace", "person/1")
+                                      .SetSchema("Person")
+                                      .SetCreationTimestampMs(1000)
+                                      .AddStringProperty("name", "John")
+                                      .AddInt64Property("age", 20)
+                                      .Build();
+  DocumentProto email_document =
+      DocumentBuilder()
+          .SetKey("namespace", "email/1")
+          .SetSchema("Email")
+          .SetCreationTimestampMs(1000)
+          .AddStringProperty("subject", "subject")
+          .AddStringProperty("sender", "namespace#person/1")
+          .Build();
+  EXPECT_THAT(icing.Put(person_document).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(email_document).status(), ProtoIsOk());
+
+  // SetSchema again with both the dummy schema types deleted.
+  // - This will cause the schema type ids of both Person and Email types to be
+  //   shifted up.
+  //
+  // This should succeed and all search features should still work after the
+  // schema id change.
+  SchemaProto schema_two =
+      SchemaBuilder()
           .AddType(SchemaTypeConfigBuilder()
                        .SetType("Person")  // schema type id 0
                        .AddProperty(PropertyConfigBuilder()
@@ -734,64 +828,24 @@
                                             JOINABLE_VALUE_TYPE_QUALIFIED_ID)
                                         .SetCardinality(CARDINALITY_REQUIRED)))
           .Build();
-
-  // Set schema with Person and Email types.
-  SetSchemaResultProto set_schema_result1 = icing.SetSchema(schema_one);
-  EXPECT_THAT(set_schema_result1.status(), ProtoStatusIs(StatusProto::OK));
-  EXPECT_THAT(set_schema_result1.latency_ms(), Eq(1000));
-
-  // Put a person document and an email document.
-  DocumentProto person_document = DocumentBuilder()
-                                      .SetKey("namespace", "person/1")
-                                      .SetSchema("Person")
-                                      .SetCreationTimestampMs(1000)
-                                      .AddStringProperty("name", "John")
-                                      .AddInt64Property("age", 20)
-                                      .Build();
-  DocumentProto email_document =
-      DocumentBuilder()
-          .SetKey("namespace", "email/1")
-          .SetSchema("Email")
-          .SetCreationTimestampMs(1000)
-          .AddStringProperty("subject", "subject")
-          .AddStringProperty("sender", "namespace#person/1")
-          .Build();
-  EXPECT_THAT(icing.Put(person_document).status(), ProtoIsOk());
-  EXPECT_THAT(icing.Put(email_document).status(), ProtoIsOk());
-
-  // SetSchema again with the schema types reordered. Schema type id will
-  // change. This should succeed and all search features should still work after
-  // schema id changed.
-  SchemaProto schema_two =
-      SchemaBuilder()
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("Email")  // schema type id 0
-                       .AddProperty(PropertyConfigBuilder()
-                                        .SetName("subject")
-                                        .SetDataTypeString(TERM_MATCH_PREFIX,
-                                                           TOKENIZER_PLAIN)
-                                        .SetCardinality(CARDINALITY_REQUIRED))
-                       .AddProperty(PropertyConfigBuilder()
-                                        .SetName("sender")
-                                        .SetDataTypeJoinableString(
-                                            JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                                        .SetCardinality(CARDINALITY_REQUIRED)))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("Person")  // schema type id 1
-                       .AddProperty(PropertyConfigBuilder()
-                                        .SetName("name")
-                                        .SetDataTypeString(TERM_MATCH_PREFIX,
-                                                           TOKENIZER_PLAIN)
-                                        .SetCardinality(CARDINALITY_REQUIRED))
-                       .AddProperty(PropertyConfigBuilder()
-                                        .SetName("age")
-                                        .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
-                                        .SetCardinality(CARDINALITY_OPTIONAL)))
-
-          .Build();
-  SetSchemaResultProto set_schema_result2 = icing.SetSchema(schema_two);
+  SetSchemaResultProto set_schema_result2 =
+      icing.SetSchema(schema_two, /*ignore_errors_and_delete_documents=*/true);
   EXPECT_THAT(set_schema_result2.status(), ProtoStatusIs(StatusProto::OK));
-  EXPECT_THAT(set_schema_result2.latency_ms(), Eq(1000));
+  EXPECT_THAT(set_schema_result2.set_schema_stats().overall_latency_ms(),
+              Eq(1000));
+  EXPECT_THAT(set_schema_result2.set_schema_stats()
+                  .schema_store_set_schema_latency_ms(),
+              Eq(1000));
+  // Since the schema types have been changed, derived files were changed. So
+  // we need to do a recovery proof flush.
+  EXPECT_THAT(set_schema_result2.needs_persist_type(),
+              Eq(PersistType::RECOVERY_PROOF));
+  // This will trigger join index restoration for JoinIndex V1 and V2.
+  if (options.enable_qualified_id_join_index_v3()) {
+    EXPECT_FALSE(set_schema_result2.has_qualified_id_join_index_restored());
+  } else {
+    EXPECT_TRUE(set_schema_result2.has_qualified_id_join_index_restored());
+  }
 
   ResultSpecProto result_spec = ResultSpecProto::default_instance();
   result_spec.set_max_joined_children_per_parent_to_return(
@@ -866,8 +920,8 @@
   // - 'b': int64 type, indexed.
   SchemaTypeConfigProto db1_type =
       SchemaTypeConfigBuilder()
-          .SetType("db1_type")
-          .SetDatabase("db1")
+          .SetType("db1/type")
+          .SetDatabase("db1/")
           .AddProperty(PropertyConfigBuilder()
                            .SetName("a")
                            .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
@@ -880,18 +934,20 @@
   SchemaProto db1_schema = SchemaBuilder().AddType(db1_type).Build();
   SetSchemaResultProto set_schema_result =
       icing.SetSchema(CreateSetSchemaRequestProto(
-          db1_schema, "db1", /*ignore_errors_and_delete_documents=*/false));
+          db1_schema, "db1/", /*ignore_errors_and_delete_documents=*/false));
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
   SetSchemaResultProto expected_set_schema_result;
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
-  expected_set_schema_result.mutable_new_schema_types()->Add("db1_type");
+  expected_set_schema_result.mutable_new_schema_types()->Add("db1/type");
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   DocumentProto db1_document =
       DocumentBuilder()
           .SetKey("namespace", "uri1")
-          .SetSchema("db1_type")
+          .SetSchema("db1/type")
           .AddStringProperty("a", "message body")
           .AddInt64Property("b", 123)
           .SetCreationTimestampMs(kDefaultCreationTimestampMs)
@@ -930,8 +986,8 @@
   // - 'c': int64 type, indexed.
   SchemaTypeConfigProto db2_type =
       SchemaTypeConfigBuilder()
-          .SetType("db2_type")
-          .SetDatabase("db2")
+          .SetType("db2/type")
+          .SetDatabase("db2/")
           .AddProperty(
               PropertyConfigBuilder()
                   .SetName("a")
@@ -944,18 +1000,22 @@
           .Build();
   SchemaProto db2_schema = SchemaBuilder().AddType(db2_type).Build();
   set_schema_result = icing.SetSchema(CreateSetSchemaRequestProto(
-      db2_schema, "db2", /*ignore_errors_and_delete_documents=*/false));
+      db2_schema, "db2/", /*ignore_errors_and_delete_documents=*/false));
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
   expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
-  expected_set_schema_result.mutable_new_schema_types()->Add("db2_type");
+  expected_set_schema_result.mutable_new_schema_types()->Add("db2/type");
+  // SetSchema db operation will preserve the existing schema type ids as much
+  // as possible. In this case, since "db1/type"'s id is unchanged, the document
+  // store and index are not updated. Therefore, no need to flush.
+  expected_set_schema_result.set_needs_persist_type(PersistType::UNKNOWN);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   DocumentProto db2_document =
       DocumentBuilder()
           .SetKey("namespace", "uri2")
-          .SetSchema("db2_type")
+          .SetSchema("db2/type")
           .AddStringProperty("a", "message body")
           .AddInt64Property("c", 123)
           .SetCreationTimestampMs(kDefaultCreationTimestampMs)
@@ -999,7 +1059,7 @@
   expected_get_schema_result_proto_db1_full.mutable_status()->set_code(
       StatusProto::OK);
   *expected_get_schema_result_proto_db1_full.mutable_schema() = db1_schema;
-  EXPECT_THAT(icing.GetSchema("db1"),
+  EXPECT_THAT(icing.GetSchema("db1/"),
               EqualsProto(expected_get_schema_result_proto_db1_full));
 
   // Get db2 schema
@@ -1007,7 +1067,188 @@
   expected_get_schema_result_proto_db2_full.mutable_status()->set_code(
       StatusProto::OK);
   *expected_get_schema_result_proto_db2_full.mutable_schema() = db2_schema;
-  EXPECT_THAT(icing.GetSchema("db2"),
+  EXPECT_THAT(icing.GetSchema("db2/"),
+              EqualsProto(expected_get_schema_result_proto_db2_full));
+}
+
+TEST_F(IcingSearchEngineSchemaTest,
+       SetSchemaWithEmptyDatabaseResetsFullSchema) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  // Create and set schema in db1 with 2 properties:
+  // - 'a': string type, indexed.
+  // - 'b': int64 type, indexed.
+  SchemaTypeConfigProto db1_type =
+      SchemaTypeConfigBuilder()
+          .SetType("db1/type")
+          .SetDatabase("db1/")
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("a")
+                           .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                           .SetCardinality(CARDINALITY_REQUIRED))
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("b")
+                           .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                           .SetCardinality(CARDINALITY_REQUIRED))
+          .Build();
+  SchemaProto db1_schema = SchemaBuilder().AddType(db1_type).Build();
+  SetSchemaResultProto set_schema_result =
+      icing.SetSchema(CreateSetSchemaRequestProto(
+          db1_schema, "db1/", /*ignore_errors_and_delete_documents=*/false));
+  // Ignore latency numbers. They're covered elsewhere.
+  set_schema_result.clear_set_schema_stats();
+  SetSchemaResultProto expected_set_schema_result;
+  expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result.mutable_new_schema_types()->Add("db1/type");
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
+  EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
+
+  DocumentProto db1_document =
+      DocumentBuilder()
+          .SetKey("namespace", "uri1")
+          .SetSchema("db1/type")
+          .AddStringProperty("a", "message body")
+          .AddInt64Property("b", 123)
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  ASSERT_THAT(icing.Put(db1_document).status(), ProtoIsOk());
+
+  SearchResultProto expected_search_result_proto_db1;
+  expected_search_result_proto_db1.mutable_status()->set_code(StatusProto::OK);
+  *expected_search_result_proto_db1.mutable_results()
+       ->Add()
+       ->mutable_document() = db1_document;
+
+  // Verify term search
+  SearchSpecProto search_spec1;
+  search_spec1.set_query("message");
+  search_spec1.set_term_match_type(TermMatchType::EXACT_ONLY);
+
+  SearchResultProto actual_results =
+      icing.Search(search_spec1, GetDefaultScoringSpec(),
+                   ResultSpecProto::default_instance());
+  EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
+                                  expected_search_result_proto_db1));
+
+  // Verify numeric (integer) search
+  SearchSpecProto search_spec2;
+  search_spec2.set_query("b == 123");
+  search_spec2.add_enabled_features(std::string(kNumericSearchFeature));
+
+  actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
+                                ResultSpecProto::default_instance());
+  EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
+                                  expected_search_result_proto_db1));
+
+  // Reset the full schema. Add a type each for db1 and db2.
+  SchemaTypeConfigProto db1_type_2 =
+      SchemaTypeConfigBuilder()
+          .SetType("db1/type_2")
+          .SetDatabase("db1/")
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("d")
+                           .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                           .SetCardinality(CARDINALITY_REQUIRED))
+          .Build();
+  SchemaTypeConfigProto db2_type =
+      SchemaTypeConfigBuilder()
+          .SetType("db2/type")
+          .SetDatabase("db2/")
+          .AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("a")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_REQUIRED))
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("c")
+                           .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                           .SetCardinality(CARDINALITY_REQUIRED))
+          .Build();
+  SchemaProto full_schema = SchemaBuilder()
+                                .AddType(db1_type)
+                                .AddType(db1_type_2)
+                                .AddType(db2_type)
+                                .Build();
+
+  // Set the full schema using an empty database.
+  SetSchemaRequestProto set_schema_request;
+  *set_schema_request.mutable_schema() = full_schema;
+  set_schema_request.set_ignore_errors_and_delete_documents(false);
+  set_schema_request.clear_database();
+  set_schema_result = icing.SetSchema(std::move(set_schema_request));
+  std::sort(set_schema_result.mutable_new_schema_types()->begin(),
+            set_schema_result.mutable_new_schema_types()->end());
+  // Ignore latency numbers. They're covered elsewhere.
+  set_schema_result.clear_set_schema_stats();
+  expected_set_schema_result = SetSchemaResultProto();
+  expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result.mutable_new_schema_types()->Add("db1/type_2");
+  expected_set_schema_result.mutable_new_schema_types()->Add("db2/type");
+  // SetSchema db operation will preserve the existing schema type ids as much
+  // as possible. In this case, since "db1/type"'s id is unchanged, the document
+  // store and index are not updated. Therefore, no need to flush.
+  expected_set_schema_result.set_needs_persist_type(PersistType::UNKNOWN);
+  EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
+
+  DocumentProto db2_document =
+      DocumentBuilder()
+          .SetKey("namespace", "uri2")
+          .SetSchema("db2/type")
+          .AddStringProperty("a", "message body")
+          .AddInt64Property("c", 123)
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  EXPECT_THAT(icing.Put(db2_document).status(), ProtoIsOk());
+
+  SearchResultProto expected_search_result_proto_both_docs;
+  expected_search_result_proto_both_docs.mutable_status()->set_code(
+      StatusProto::OK);
+  *expected_search_result_proto_both_docs.mutable_results()
+       ->Add()
+       ->mutable_document() = db2_document;
+  *expected_search_result_proto_both_docs.mutable_results()
+       ->Add()
+       ->mutable_document() = db1_document;
+
+  // Verify term search: will get both documents for query "a:message".
+  actual_results = icing.Search(search_spec1, GetDefaultScoringSpec(),
+                                ResultSpecProto::default_instance());
+  EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
+                                  expected_search_result_proto_both_docs));
+
+  // Verify numeric (integer) search: will only get first document for query
+  // "b == 123".
+  actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
+                                ResultSpecProto::default_instance());
+  EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
+                                  expected_search_result_proto_db1));
+
+  // Get full schema
+  GetSchemaResultProto expected_get_schema_result_proto_full;
+  expected_get_schema_result_proto_full.mutable_status()->set_code(
+      StatusProto::OK);
+  *expected_get_schema_result_proto_full.mutable_schema() = full_schema;
+  EXPECT_THAT(icing.GetSchema(),
+              EqualsProto(expected_get_schema_result_proto_full));
+
+  // Get db1 schema
+  GetSchemaResultProto expected_get_schema_result_proto_db1_full;
+  expected_get_schema_result_proto_db1_full.mutable_status()->set_code(
+      StatusProto::OK);
+  *expected_get_schema_result_proto_db1_full.mutable_schema() =
+      SchemaBuilder().AddType(db1_type).AddType(db1_type_2).Build();
+  EXPECT_THAT(icing.GetSchema("db1/"),
+              EqualsProto(expected_get_schema_result_proto_db1_full));
+
+  // Get db2 schema
+  GetSchemaResultProto expected_get_schema_result_proto_db2_full;
+  expected_get_schema_result_proto_db2_full.mutable_status()->set_code(
+      StatusProto::OK);
+  *expected_get_schema_result_proto_db2_full.mutable_schema() =
+      SchemaBuilder().AddType(db2_type).Build();
+  EXPECT_THAT(icing.GetSchema("db2/"),
               EqualsProto(expected_get_schema_result_proto_db2_full));
 }
 
@@ -1020,8 +1261,8 @@
   // - 'c': int64 type, indexed.
   SchemaTypeConfigProto db1_type =
       SchemaTypeConfigBuilder()
-          .SetType("db1_type")
-          .SetDatabase("db1")
+          .SetType("db1/type")
+          .SetDatabase("db1/")
           .AddProperty(PropertyConfigBuilder()
                            .SetName("b")
                            .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
@@ -1034,12 +1275,14 @@
   SchemaProto db1_schema = SchemaBuilder().AddType(db1_type).Build();
   SetSchemaResultProto set_schema_result =
       icing.SetSchema(CreateSetSchemaRequestProto(
-          db1_schema, "db1", /*ignore_errors_and_delete_documents=*/false));
+          db1_schema, "db1/", /*ignore_errors_and_delete_documents=*/false));
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
   SetSchemaResultProto expected_set_schema_result;
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
-  expected_set_schema_result.mutable_new_schema_types()->Add("db1_type");
+  expected_set_schema_result.mutable_new_schema_types()->Add("db1/type");
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Add a schema for db2:
@@ -1047,8 +1290,8 @@
   // - 'd': int64 type, indexed.
   SchemaTypeConfigProto db2_type =
       SchemaTypeConfigBuilder()
-          .SetType("db2_type")
-          .SetDatabase("db2")
+          .SetType("db2/type")
+          .SetDatabase("db2/")
           .AddProperty(
               PropertyConfigBuilder()
                   .SetName("b")
@@ -1061,19 +1304,23 @@
           .Build();
   SchemaProto db2_schema = SchemaBuilder().AddType(db2_type).Build();
   set_schema_result = icing.SetSchema(CreateSetSchemaRequestProto(
-      db2_schema, "db2", /*ignore_errors_and_delete_documents=*/false));
+      db2_schema, "db2/", /*ignore_errors_and_delete_documents=*/false));
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
   expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
-  expected_set_schema_result.mutable_new_schema_types()->Add("db2_type");
+  expected_set_schema_result.mutable_new_schema_types()->Add("db2/type");
+  // SetSchema db operation will preserve the existing schema type ids as much
+  // as possible. In this case, since "db1/type"'s id is unchanged, the document
+  // store and index are not updated. Therefore, no need to flush.
+  expected_set_schema_result.set_needs_persist_type(PersistType::UNKNOWN);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Add documents
   DocumentProto db1_document1 =
       DocumentBuilder()
           .SetKey("namespace", "uri1")
-          .SetSchema("db1_type")
+          .SetSchema("db1/type")
           .AddStringProperty("b", "message body")
           .AddInt64Property("c", 123)
           .SetCreationTimestampMs(kDefaultCreationTimestampMs)
@@ -1081,7 +1328,7 @@
   DocumentProto db2_document =
       DocumentBuilder()
           .SetKey("namespace", "uri2")
-          .SetSchema("db2_type")
+          .SetSchema("db2/type")
           .AddStringProperty("b", "message body")
           .AddInt64Property("d", 123)
           .SetCreationTimestampMs(kDefaultCreationTimestampMs)
@@ -1138,20 +1385,28 @@
           .Build());
   db1_schema = SchemaBuilder().AddType(db1_type).Build();
   set_schema_result = icing.SetSchema(CreateSetSchemaRequestProto(
-      db1_schema, "db1", /*ignore_errors_and_delete_documents=*/false));
+      db1_schema, "db1/", /*ignore_errors_and_delete_documents=*/false));
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
   expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
   expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
-      ->Add("db1_type");
+      ->Add("db1/type");
+  expected_set_schema_result.set_has_term_index_restored(true);
+  expected_set_schema_result.set_has_integer_index_restored(true);
+  expected_set_schema_result.set_has_embedding_index_restored(true);
+  expected_set_schema_result.set_has_qualified_id_join_index_restored(false);
+  // Since it is an index incompatible change, we need to do a recovery proof
+  // flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Add new document
   DocumentProto db1_document2 =
       DocumentBuilder()
           .SetKey("namespace", "uri3")
-          .SetSchema("db1_type")
+          .SetSchema("db1/type")
           .AddStringProperty("a", "message body")
           .AddStringProperty("b", "string value")
           .AddInt64Property("c", 123)
@@ -1214,7 +1469,7 @@
   expected_get_schema_result_proto_db1_full.mutable_status()->set_code(
       StatusProto::OK);
   *expected_get_schema_result_proto_db1_full.mutable_schema() = db1_schema;
-  EXPECT_THAT(icing.GetSchema("db1"),
+  EXPECT_THAT(icing.GetSchema("db1/"),
               EqualsProto(expected_get_schema_result_proto_db1_full));
 
   // Get db2 schema
@@ -1222,12 +1477,13 @@
   expected_get_schema_result_proto_db2_full.mutable_status()->set_code(
       StatusProto::OK);
   *expected_get_schema_result_proto_db2_full.mutable_schema() = db2_schema;
-  EXPECT_THAT(icing.GetSchema("db2"),
+  EXPECT_THAT(icing.GetSchema("db2/"),
               EqualsProto(expected_get_schema_result_proto_db2_full));
 }
 
 TEST_F(IcingSearchEngineSchemaTest, SetSchemaEmptySchemaClearsDatabase) {
-  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  IcingSearchEngine icing(options, GetTestJniCache());
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
 
   // Create and set schema in db1 with 2 properties:
@@ -1235,8 +1491,8 @@
   // - 'c': int64 type, indexed.
   SchemaTypeConfigProto db1_type =
       SchemaTypeConfigBuilder()
-          .SetType("db1_type")
-          .SetDatabase("db1")
+          .SetType("db1/type")
+          .SetDatabase("db1/")
           .AddProperty(PropertyConfigBuilder()
                            .SetName("b")
                            .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
@@ -1249,12 +1505,14 @@
   SchemaProto db1_schema = SchemaBuilder().AddType(db1_type).Build();
   SetSchemaResultProto set_schema_result =
       icing.SetSchema(CreateSetSchemaRequestProto(
-          db1_schema, "db1", /*ignore_errors_and_delete_documents=*/true));
+          db1_schema, "db1/", /*ignore_errors_and_delete_documents=*/true));
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
   SetSchemaResultProto expected_set_schema_result;
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
-  expected_set_schema_result.mutable_new_schema_types()->Add("db1_type");
+  expected_set_schema_result.mutable_new_schema_types()->Add("db1/type");
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Add a schema for db2:
@@ -1262,8 +1520,8 @@
   // - 'd': int64 type, indexed.
   SchemaTypeConfigProto db2_type =
       SchemaTypeConfigBuilder()
-          .SetType("db2_type")
-          .SetDatabase("db2")
+          .SetType("db2/type")
+          .SetDatabase("db2/")
           .AddProperty(
               PropertyConfigBuilder()
                   .SetName("b")
@@ -1276,19 +1534,23 @@
           .Build();
   SchemaProto db2_schema = SchemaBuilder().AddType(db2_type).Build();
   set_schema_result = icing.SetSchema(CreateSetSchemaRequestProto(
-      db2_schema, "db2", /*ignore_errors_and_delete_documents=*/true));
+      db2_schema, "db2/", /*ignore_errors_and_delete_documents=*/true));
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
   expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
-  expected_set_schema_result.mutable_new_schema_types()->Add("db2_type");
+  expected_set_schema_result.mutable_new_schema_types()->Add("db2/type");
+  // SetSchema db operation will preserve the existing schema type ids as much
+  // as possible. In this case, since "db1/type"'s id is unchanged, the document
+  // store and index are not updated. Therefore, no need to flush.
+  expected_set_schema_result.set_needs_persist_type(PersistType::UNKNOWN);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Add documents
   DocumentProto db1_document1 =
       DocumentBuilder()
           .SetKey("namespace", "uri1")
-          .SetSchema("db1_type")
+          .SetSchema("db1/type")
           .AddStringProperty("b", "message body")
           .AddInt64Property("c", 123)
           .SetCreationTimestampMs(kDefaultCreationTimestampMs)
@@ -1296,7 +1558,7 @@
   DocumentProto db2_document =
       DocumentBuilder()
           .SetKey("namespace", "uri2")
-          .SetSchema("db2_type")
+          .SetSchema("db2/type")
           .AddStringProperty("b", "message body")
           .AddInt64Property("d", 123)
           .SetCreationTimestampMs(kDefaultCreationTimestampMs)
@@ -1344,19 +1606,31 @@
   //   - 'd': int64 type, indexed.
   db1_schema = SchemaProto();
   set_schema_result = icing.SetSchema(CreateSetSchemaRequestProto(
-      db1_schema, "db1", /*ignore_errors_and_delete_documents=*/true));
+      db1_schema, "db1/", /*ignore_errors_and_delete_documents=*/true));
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
   expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
-  expected_set_schema_result.mutable_deleted_schema_types()->Add("db1_type");
+  expected_set_schema_result.mutable_deleted_schema_types()->Add("db1/type");
+  expected_set_schema_result.set_deleted_document_count(1);
+  // Since a document was deleted, we need to do a recovery proof flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
+  // Deleting db1_schema results in a schema type id reassignment, which would
+  // trigger join index restoration if using join index v1 and v2.
+  if (!options.enable_qualified_id_join_index_v3()) {
+    expected_set_schema_result.set_has_qualified_id_join_index_restored(true);
+    expected_set_schema_result.set_has_term_index_restored(false);
+    expected_set_schema_result.set_has_integer_index_restored(false);
+    expected_set_schema_result.set_has_embedding_index_restored(false);
+  }
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Adding new document fails because db1_type is deleted.
   DocumentProto db1_document2 =
       DocumentBuilder()
           .SetKey("namespace", "uri3")
-          .SetSchema("db1_type")
+          .SetSchema("db1/type")
           .AddStringProperty("a", "message body")
           .AddStringProperty("b", "string value")
           .AddInt64Property("c", 123)
@@ -1405,7 +1679,7 @@
               EqualsProto(expected_get_schema_result_proto_full));
 
   // Get db1 schema should return NOT_FOUND.
-  EXPECT_THAT(icing.GetSchema("db1").status(),
+  EXPECT_THAT(icing.GetSchema("db1/").status(),
               ProtoStatusIs(StatusProto::NOT_FOUND));
 
   // Get db2 schema
@@ -1413,7 +1687,7 @@
   expected_get_schema_result_proto_db2_full.mutable_status()->set_code(
       StatusProto::OK);
   *expected_get_schema_result_proto_db2_full.mutable_schema() = db2_schema;
-  EXPECT_THAT(icing.GetSchema("db2"),
+  EXPECT_THAT(icing.GetSchema("db2/"),
               EqualsProto(expected_get_schema_result_proto_db2_full));
 }
 
@@ -1442,10 +1716,13 @@
 
   SetSchemaResultProto set_schema_result = icing.SetSchema(schema_one);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   SetSchemaResultProto expected_set_schema_result;
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
   expected_set_schema_result.mutable_new_schema_types()->Add("Schema");
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   DocumentProto document =
@@ -1505,11 +1782,20 @@
   // restoration should use new section ids to rebuild.
   set_schema_result = icing.SetSchema(schema_two);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
   expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
       ->Add("Schema");
+  expected_set_schema_result.set_has_term_index_restored(true);
+  expected_set_schema_result.set_has_integer_index_restored(true);
+  expected_set_schema_result.set_has_qualified_id_join_index_restored(false);
+  expected_set_schema_result.set_has_embedding_index_restored(true);
+  // Since it is an index incompatible change, we need to do a recovery proof
+  // flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Verify term search: will get document now.
@@ -1551,10 +1837,13 @@
 
   SetSchemaResultProto set_schema_result = icing.SetSchema(schema_one);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   SetSchemaResultProto expected_set_schema_result;
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
   expected_set_schema_result.mutable_new_schema_types()->Add("Schema");
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   DocumentProto document =
@@ -1610,11 +1899,20 @@
   // restoration should use new section ids to rebuild.
   set_schema_result = icing.SetSchema(schema_two);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
   expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
       ->Add("Schema");
+  expected_set_schema_result.set_has_term_index_restored(true);
+  expected_set_schema_result.set_has_integer_index_restored(true);
+  expected_set_schema_result.set_has_qualified_id_join_index_restored(false);
+  expected_set_schema_result.set_has_embedding_index_restored(true);
+  // Since it is an index incompatible change, we need to do a recovery proof
+  // flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Verify term search: will still get document.
@@ -1839,13 +2137,22 @@
   // restoration should use new section ids to rebuild.
   SetSchemaResultProto set_schema_result = icing.SetSchema(schema_two);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   SetSchemaResultProto expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
   expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
       ->Add("Person");
   expected_set_schema_result.mutable_join_incompatible_changed_schema_types()
       ->Add("Person");
+  expected_set_schema_result.set_has_term_index_restored(true);
+  expected_set_schema_result.set_has_integer_index_restored(true);
+  expected_set_schema_result.set_has_qualified_id_join_index_restored(true);
+  expected_set_schema_result.set_has_embedding_index_restored(true);
+  // Since it is an index incompatible change, we need to do a recovery proof
+  // flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Verify term search:
@@ -1925,11 +2232,14 @@
 
   SetSchemaResultProto set_schema_result = icing.SetSchema(nested_schema);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   SetSchemaResultProto expected_set_schema_result;
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
   expected_set_schema_result.mutable_new_schema_types()->Add("Email");
   expected_set_schema_result.mutable_new_schema_types()->Add("Person");
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   DocumentProto document =
@@ -2027,11 +2337,20 @@
 
   set_schema_result = icing.SetSchema(no_nested_schema);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
   expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
       ->Add("Email");
+  expected_set_schema_result.set_has_term_index_restored(true);
+  expected_set_schema_result.set_has_integer_index_restored(true);
+  expected_set_schema_result.set_has_qualified_id_join_index_restored(false);
+  expected_set_schema_result.set_has_embedding_index_restored(true);
+  // Since it is an index incompatible change, we need to do a recovery proof
+  // flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Verify term search
@@ -2145,11 +2464,14 @@
 
   SetSchemaResultProto set_schema_result = icing.SetSchema(nested_schema);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   SetSchemaResultProto expected_set_schema_result;
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
   expected_set_schema_result.mutable_new_schema_types()->Add("Email");
   expected_set_schema_result.mutable_new_schema_types()->Add("Person");
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   DocumentProto document =
@@ -2317,11 +2639,20 @@
 
   set_schema_result = icing.SetSchema(nested_schema_with_less_props);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
   expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
       ->Add("Email");
+  expected_set_schema_result.set_has_term_index_restored(true);
+  expected_set_schema_result.set_has_integer_index_restored(true);
+  expected_set_schema_result.set_has_qualified_id_join_index_restored(false);
+  expected_set_schema_result.set_has_embedding_index_restored(true);
+  // Since it is an index incompatible change, we need to do a recovery proof
+  // flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Verify term search
@@ -2380,11 +2711,14 @@
 
   SetSchemaResultProto set_schema_result = icing.SetSchema(schema_one);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   SetSchemaResultProto expected_set_schema_result;
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
   expected_set_schema_result.mutable_new_schema_types()->Add("Message");
   expected_set_schema_result.mutable_new_schema_types()->Add("Person");
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   DocumentProto person1 =
@@ -2496,11 +2830,20 @@
   // index restoration should use new joinable property ids to rebuild.
   set_schema_result = icing.SetSchema(schema_two);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
   expected_set_schema_result.mutable_join_incompatible_changed_schema_types()
       ->Add("Message");
+  expected_set_schema_result.set_has_term_index_restored(false);
+  expected_set_schema_result.set_has_integer_index_restored(false);
+  expected_set_schema_result.set_has_qualified_id_join_index_restored(true);
+  expected_set_schema_result.set_has_embedding_index_restored(false);
+  // Since it is a join incompatible change, we need to do a recovery proof
+  // flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Verify join search: join a query for `name:person` with a child query for
@@ -2658,10 +3001,13 @@
   SetSchemaResultProto set_schema_result =
       icing.SetSchema(email_with_body_schema);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   SetSchemaResultProto expected_set_schema_result;
   expected_set_schema_result.mutable_new_schema_types()->Add("Email");
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Create a document with only subject and timestamp2 property.
@@ -2733,12 +3079,22 @@
   set_schema_result = icing.SetSchema(
       email_no_body_schema, /*ignore_errors_and_delete_documents=*/true);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_incompatible_schema_types()->Add("Email");
   expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
       ->Add("Email");
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result.set_deleted_document_count(0);
+  expected_set_schema_result.set_has_term_index_restored(true);
+  expected_set_schema_result.set_has_integer_index_restored(true);
+  expected_set_schema_result.set_has_qualified_id_join_index_restored(false);
+  expected_set_schema_result.set_has_embedding_index_restored(true);
+  // Since it is an index incompatible change, we need to do a recovery proof
+  // flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Verify term search
@@ -2799,11 +3155,14 @@
   SetSchemaResultProto set_schema_result =
       icing.SetSchema(email_with_receiver_schema);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   SetSchemaResultProto expected_set_schema_result;
   expected_set_schema_result.mutable_new_schema_types()->Add("Email");
   expected_set_schema_result.mutable_new_schema_types()->Add("Person");
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   DocumentProto person = DocumentBuilder()
@@ -2899,12 +3258,22 @@
       icing.SetSchema(email_without_receiver_schema,
                       /*ignore_errors_and_delete_documents=*/true);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_incompatible_schema_types()->Add("Email");
   expected_set_schema_result.mutable_join_incompatible_changed_schema_types()
       ->Add("Email");
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result.set_deleted_document_count(0);
+  expected_set_schema_result.set_has_term_index_restored(false);
+  expected_set_schema_result.set_has_integer_index_restored(false);
+  expected_set_schema_result.set_has_qualified_id_join_index_restored(true);
+  expected_set_schema_result.set_has_embedding_index_restored(false);
+  // Since it is a join incompatible change, we need to do a recovery proof
+  // flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Verify join search: join a query for `name:person` with a child query for
@@ -2949,10 +3318,13 @@
   SetSchemaResultProto set_schema_result =
       icing.SetSchema(email_with_body_schema);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   SetSchemaResultProto expected_set_schema_result;
   expected_set_schema_result.mutable_new_schema_types()->Add("Email");
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Create a document with only subject and timestamp property.
@@ -3030,12 +3402,22 @@
   set_schema_result = icing.SetSchema(
       email_no_body_schema, /*ignore_errors_and_delete_documents=*/true);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_incompatible_schema_types()->Add("Email");
   expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
       ->Add("Email");
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result.set_deleted_document_count(0);
+  expected_set_schema_result.set_has_term_index_restored(true);
+  expected_set_schema_result.set_has_integer_index_restored(true);
+  expected_set_schema_result.set_has_qualified_id_join_index_restored(false);
+  expected_set_schema_result.set_has_embedding_index_restored(true);
+  // Since it is an index incompatible change, we need to do a recovery proof
+  // flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Verify term search
@@ -3097,11 +3479,14 @@
   SetSchemaResultProto set_schema_result =
       icing.SetSchema(email_with_body_schema);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   SetSchemaResultProto expected_set_schema_result;
   expected_set_schema_result.mutable_new_schema_types()->Add("Email");
   expected_set_schema_result.mutable_new_schema_types()->Add("Person");
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   DocumentProto person = DocumentBuilder()
@@ -3199,12 +3584,22 @@
   set_schema_result = icing.SetSchema(
       email_no_body_schema, /*ignore_errors_and_delete_documents=*/true);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_incompatible_schema_types()->Add("Email");
   expected_set_schema_result.mutable_join_incompatible_changed_schema_types()
       ->Add("Email");
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result.set_deleted_document_count(0);
+  expected_set_schema_result.set_has_term_index_restored(false);
+  expected_set_schema_result.set_has_integer_index_restored(false);
+  expected_set_schema_result.set_has_qualified_id_join_index_restored(true);
+  expected_set_schema_result.set_has_embedding_index_restored(false);
+  // Since it is a join incompatible change, we need to do a recovery proof
+  // flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Verify join search: join a query for `name:person` with a child query for
@@ -3255,11 +3650,14 @@
 
   SetSchemaResultProto set_schema_result = icing.SetSchema(nested_schema);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   SetSchemaResultProto expected_set_schema_result;
   expected_set_schema_result.mutable_new_schema_types()->Add("Email");
   expected_set_schema_result.mutable_new_schema_types()->Add("Person");
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Create two documents - a person document and an email document - both docs
@@ -3312,7 +3710,8 @@
   set_schema_result = icing.SetSchema(
       nested_schema, /*ignore_errors_and_delete_documents=*/true);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_incompatible_schema_types()->Add("Person");
   expected_set_schema_result.mutable_incompatible_schema_types()->Add("Email");
@@ -3321,6 +3720,15 @@
   expected_set_schema_result.mutable_index_incompatible_changed_schema_types()
       ->Add("Person");
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result.set_deleted_document_count(2);
+  expected_set_schema_result.set_has_term_index_restored(true);
+  expected_set_schema_result.set_has_integer_index_restored(true);
+  expected_set_schema_result.set_has_qualified_id_join_index_restored(false);
+  expected_set_schema_result.set_has_embedding_index_restored(true);
+  // Since it is an index incompatible change, we need to do a recovery proof
+  // flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Both documents should be deleted now.
@@ -3333,6 +3741,141 @@
   EXPECT_THAT(get_result.status(), ProtoStatusIs(StatusProto::NOT_FOUND));
 }
 
+TEST_F(
+    IcingSearchEngineSchemaTest,
+    SetSchemaAddDeletePropagationTriggersIndexRestorationAndRevalidatesDependency) {
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_qualified_id_join_index_v3(true);
+  options.set_enable_soft_index_restoration(true);
+  options.set_enable_delete_propagation_from(true);
+  options.set_expired_document_purge_threshold_ms(0);
+
+  auto fake_clock = std::make_unique<FakeClock>();
+  FakeClock* fake_clock_ptr = fake_clock.get();
+
+  TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  // Create "Email" schema with a joinable property "senderQualifiedId" and
+  // delete propagation disabled.
+  SchemaProto schema1 =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("subject")
+                                        .SetDataTypeString(TERM_MATCH_PREFIX,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("senderQualifiedId")
+                                        .SetDataTypeJoinableString(
+                                            JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                            DELETE_PROPAGATION_TYPE_NONE)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  SetSchemaResultProto set_schema_result = icing.SetSchema(schema1);
+  // Ignore latency numbers. They're covered elsewhere.
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
+  SetSchemaResultProto expected_set_schema_result;
+  expected_set_schema_result.mutable_new_schema_types()->Add("Email");
+  expected_set_schema_result.mutable_new_schema_types()->Add("Person");
+  expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
+  EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
+
+  DocumentProto person = DocumentBuilder()
+                             .SetKey("namespace", "person")
+                             .SetSchema("Person")
+                             .SetCreationTimestampMs(10)
+                             .SetTtlMs(10000)  // Expire at t = 10010.
+                             .AddStringProperty("name", "person")
+                             .Build();
+  // Create an email document.
+  DocumentProto email =
+      DocumentBuilder()
+          .SetKey("namespace", "email")
+          .SetSchema("Email")
+          .SetCreationTimestampMs(10)
+          .SetTtlMs(0)  // Never expire.
+          .AddStringProperty("subject", "test")
+          .AddStringProperty("senderQualifiedId", "namespace#person")
+          .Build();
+
+  ASSERT_THAT(icing.Put(person).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email).status(), ProtoIsOk());
+
+  // Now update the schema to enable delete propagation for "senderQualifiedId".
+  // It is:
+  // - Schema compatible.
+  // - Join incompatible. This will trigger index restoration, and revalidation
+  //   of dependency. If any child documents are referring to a non-alive
+  //   parent, then they will be deleted.
+  SchemaProto schema2 =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("senderQualifiedId")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  // Adjust the timestamp to t = 20000 so that the person document is expired
+  // during the schema update.
+  fake_clock_ptr->SetSystemTimeMilliseconds(20000);
+  set_schema_result = icing.SetSchema(schema2);
+  // Ignore latency numbers. They're covered elsewhere.
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
+  expected_set_schema_result = SetSchemaResultProto();
+  expected_set_schema_result.mutable_join_incompatible_changed_schema_types()
+      ->Add("Email");
+  expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result.set_has_term_index_restored(false);
+  expected_set_schema_result.set_has_integer_index_restored(false);
+  expected_set_schema_result.set_has_qualified_id_join_index_restored(true);
+  expected_set_schema_result.set_has_embedding_index_restored(false);
+  // Since it is a join incompatible change, we need to do a recovery proof
+  // flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
+  EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
+
+  // Attempt to get the email document. It should be deleted due to the
+  // dependency on the expired person document.
+  GetResultProto expected_get_result_proto;
+  expected_get_result_proto.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto.mutable_status()->set_message(
+      "Document (namespace, email) not found.");
+  EXPECT_THAT(
+      icing.Get("namespace", "email", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto));
+}
+
 TEST_F(IcingSearchEngineSchemaTest, SetSchemaRevalidatesDocumentsAndReturnsOk) {
   IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
@@ -3381,14 +3924,14 @@
   SetSchemaResultProto set_schema_result =
       icing.SetSchema(schema_with_required_subject);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   SetSchemaResultProto expected_set_schema_result_proto;
   expected_set_schema_result_proto.mutable_status()->set_code(
       StatusProto::FAILED_PRECONDITION);
   expected_set_schema_result_proto.mutable_status()->set_message(
       "Schema is incompatible.");
   expected_set_schema_result_proto.add_incompatible_schema_types("email");
-
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result_proto));
 
   // Force set it
@@ -3396,9 +3939,14 @@
       icing.SetSchema(schema_with_required_subject,
                       /*ignore_errors_and_delete_documents=*/true);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   expected_set_schema_result_proto.mutable_status()->set_code(StatusProto::OK);
   expected_set_schema_result_proto.mutable_status()->clear_message();
+  expected_set_schema_result_proto.set_deleted_document_count(1);
+  // Since a document was deleted, we need to do a recovery proof flush.
+  expected_set_schema_result_proto.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result_proto));
 
   GetResultProto expected_get_result_proto;
@@ -3458,7 +4006,8 @@
   // Can't set the schema since it's incompatible
   SetSchemaResultProto set_schema_result = icing.SetSchema(new_schema);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   SetSchemaResultProto expected_result;
   expected_result.mutable_status()->set_code(StatusProto::FAILED_PRECONDITION);
   expected_result.mutable_status()->set_message("Schema is incompatible.");
@@ -3471,9 +4020,13 @@
       icing.SetSchema(new_schema,
                       /*ignore_errors_and_delete_documents=*/true);
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   expected_result.mutable_status()->set_code(StatusProto::OK);
   expected_result.mutable_status()->clear_message();
+  expected_result.set_deleted_document_count(1);
+  // Since a document was deleted, we need to do a recovery proof flush.
+  expected_result.set_needs_persist_type(PersistType::RECOVERY_PROOF);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_result));
 
   // "email" document is still there
@@ -3523,8 +4076,8 @@
   // Add a schema for db1:
   SchemaTypeConfigProto db1_type =
       SchemaTypeConfigBuilder()
-          .SetType("db1_type")
-          .SetDatabase("db1")
+          .SetType("db1/type")
+          .SetDatabase("db1/")
           .AddProperty(PropertyConfigBuilder()
                            .SetName("a")
                            .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
@@ -3538,19 +4091,21 @@
   SchemaProto db1_schema = SchemaBuilder().AddType(db1_type).Build();
   SetSchemaResultProto set_schema_result =
       icing.SetSchema(CreateSetSchemaRequestProto(
-          db1_schema, "db1", /*ignore_errors_and_delete_documents=*/false));
+          db1_schema, "db1/", /*ignore_errors_and_delete_documents=*/false));
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
   SetSchemaResultProto expected_set_schema_result;
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
-  expected_set_schema_result.mutable_new_schema_types()->Add("db1_type");
+  expected_set_schema_result.mutable_new_schema_types()->Add("db1/type");
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // Add a schema for db2:
   SchemaTypeConfigProto db2_type =
       SchemaTypeConfigBuilder()
-          .SetType("db2_type")
-          .SetDatabase("db2")
+          .SetType("db2/type")
+          .SetDatabase("db2/")
           .AddProperty(
               PropertyConfigBuilder()
                   .SetName("a")
@@ -3564,12 +4119,17 @@
 
   SchemaProto db2_schema = SchemaBuilder().AddType(db2_type).Build();
   set_schema_result = icing.SetSchema(CreateSetSchemaRequestProto(
-      db2_schema, "db2", /*ignore_errors_and_delete_documents=*/false));
+      db2_schema, "db2/", /*ignore_errors_and_delete_documents=*/false));
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
   expected_set_schema_result = SetSchemaResultProto();
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
-  expected_set_schema_result.mutable_new_schema_types()->Add("db2_type");
+  expected_set_schema_result.mutable_new_schema_types()->Add("db2/type");
+  // No need to flush since:
+  // - Schema type id is not changed.
+  // - No document is deleted.
+  // - Only a new schema type is added and indices are not rebuilt.
+  expected_set_schema_result.set_needs_persist_type(PersistType::UNKNOWN);
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   // GetSchema per database
@@ -3577,13 +4137,13 @@
   expected_get_schema_result_proto_db1.mutable_status()->set_code(
       StatusProto::OK);
   *expected_get_schema_result_proto_db1.mutable_schema() = db1_schema;
-  EXPECT_THAT(icing.GetSchema("db1"),
+  EXPECT_THAT(icing.GetSchema("db1/"),
               EqualsProto(expected_get_schema_result_proto_db1));
   GetSchemaResultProto expected_get_schema_result_proto_db2;
   expected_get_schema_result_proto_db2.mutable_status()->set_code(
       StatusProto::OK);
   *expected_get_schema_result_proto_db2.mutable_schema() = db2_schema;
-  EXPECT_THAT(icing.GetSchema("db2"),
+  EXPECT_THAT(icing.GetSchema("db2/"),
               EqualsProto(expected_get_schema_result_proto_db2));
 
   // Get full schema
@@ -3611,8 +4171,8 @@
   SchemaProto db1_schema =
       SchemaBuilder()
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_type")
-                       .SetDatabase("db1")
+                       .SetType("db1/type")
+                       .SetDatabase("db1/")
                        .AddProperty(PropertyConfigBuilder()
                                         .SetName("a")
                                         .SetDataTypeString(TERM_MATCH_EXACT,
@@ -3625,12 +4185,15 @@
           .Build();
   SetSchemaResultProto set_schema_result =
       icing.SetSchema(CreateSetSchemaRequestProto(
-          db1_schema, "db1", /*ignore_errors_and_delete_documents=*/false));
+          db1_schema, "db1/", /*ignore_errors_and_delete_documents=*/false));
   // Ignore latency numbers. They're covered elsewhere.
-  set_schema_result.clear_latency_ms();
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
   SetSchemaResultProto expected_set_schema_result;
   expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
-  expected_set_schema_result.mutable_new_schema_types()->Add("db1_type");
+  expected_set_schema_result.mutable_new_schema_types()->Add("db1/type");
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::UNKNOWN);  // Flush is not needed for the first SetSchema.
   EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   get_schema_result_proto = icing.GetSchema("nonexistent_db");
@@ -3703,7 +4266,11 @@
       StatusProto::OK);
   *expected_get_schema_type_result_proto.mutable_schema_type_config() =
       schema_type_config;
-  EXPECT_THAT(icing.GetSchemaType("SchemaType"),
+  GetSchemaTypeResultProto actual_get_schema_type_result_proto =
+      icing.GetSchemaType("SchemaType");
+  actual_get_schema_type_result_proto
+      .clear_vm_binder_transaction_latency_start_time_ms();
+  EXPECT_THAT(actual_get_schema_type_result_proto,
               EqualsProto(expected_get_schema_type_result_proto));
 }
 
@@ -3790,7 +4357,6 @@
   // Setting the new, different schema will remove incompatible documents
   IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
   EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
-  EXPECT_THAT(icing.SetSchema(incompatible_schema).status(), ProtoIsOk());
 
   // Can't retrieve by namespace/uri
   GetResultProto expected_get_result_proto;
@@ -4152,6 +4718,113 @@
               Eq(IcingSearchEngineMarkerProto::OperationType::SET_SCHEMA));
 }
 
+TEST_F(IcingSearchEngineSchemaTest, SetSchemaStatsArePopulated) {
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetTimerElapsedMilliseconds(1000);
+  TestIcingSearchEngine icing(GetDefaultIcingOptions(),
+                              std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  SchemaProto message_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Message")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("body")
+                                        .SetDataTypeString(TERM_MATCH_UNKNOWN,
+                                                           TOKENIZER_NONE)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("indexableInteger")
+                                        .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                        .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+
+  SetSchemaRequestProto set_schema_request =
+      CreateSetSchemaRequestProto(message_schema, /*database=*/"",
+                                  /*ignore_errors_and_delete_documents=*/false);
+  SetSchemaResultProto set_schema_result =
+      icing.SetSchema(std::move(set_schema_request));
+  EXPECT_THAT(set_schema_result.status(), ProtoStatusIs(StatusProto::OK));
+  EXPECT_THAT(set_schema_result.set_schema_stats().overall_latency_ms(),
+              Eq(1000));
+  EXPECT_THAT(
+      set_schema_result.set_schema_stats().schema_store_set_schema_latency_ms(),
+      Eq(1000));
+
+  // Put a document to trigger index restoration later.
+  DocumentProto message_document =
+      DocumentBuilder()
+          .SetKey("namespace", "message")
+          .SetSchema("Message")
+          .AddStringProperty("body", "test body")
+          .AddInt64Property("indexableInteger", 123)
+          .Build();
+  EXPECT_THAT(icing.Put(message_document).status(),
+              ProtoStatusIs(StatusProto::OK));
+
+  // This schema update will trigger and set latencies for index restoration and
+  // scorable property cache regeneration.
+  SchemaProto updated_schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Message")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("body")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_REQUIRED))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("indexableInteger")
+                                   .SetDataTypeInt64(NUMERIC_MATCH_UNKNOWN)
+                                   .SetCardinality(CARDINALITY_REQUIRED))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("score")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("senderQualifiedId")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+  SetSchemaRequestProto set_schema_request2 =
+      CreateSetSchemaRequestProto(updated_schema, /*database=*/"",
+                                  /*ignore_errors_and_delete_documents=*/false);
+  set_schema_result = icing.SetSchema(std::move(set_schema_request2));
+  EXPECT_THAT(set_schema_result.status(), ProtoStatusIs(StatusProto::OK));
+  EXPECT_THAT(set_schema_result.set_schema_stats().overall_latency_ms(),
+              Eq(1000));
+  EXPECT_THAT(
+      set_schema_result.set_schema_stats().index_restoration_latency_ms(),
+      Eq(1000));
+  EXPECT_THAT(set_schema_result.set_schema_stats()
+                  .scorable_property_cache_regeneration_latency_ms(),
+              Eq(1000));
+
+  // Clear schema -- this should trigger and update document store and set
+  // latencies for document store update schema
+  SchemaProto empty_schema = SchemaBuilder().Build();
+  SetSchemaRequestProto set_schema_request3 =
+      CreateSetSchemaRequestProto(empty_schema, /*database=*/"",
+                                  /*ignore_errors_and_delete_documents=*/true);
+  set_schema_result = icing.SetSchema(std::move(set_schema_request3));
+  EXPECT_THAT(set_schema_result.status(), ProtoStatusIs(StatusProto::OK));
+  EXPECT_THAT(set_schema_result.set_schema_stats().overall_latency_ms(),
+              Eq(1000));
+  EXPECT_THAT(
+      set_schema_result.set_schema_stats().schema_store_set_schema_latency_ms(),
+      Eq(1000));
+  EXPECT_THAT(set_schema_result.set_schema_stats()
+                  .document_store_optimized_update_schema_latency_ms(),
+              Eq(1000));
+}
+
 }  // namespace
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/icing-search-engine_search_test.cc b/icing/icing-search-engine_search_test.cc
index 46040a4..0835439 100644
--- a/icing/icing-search-engine_search_test.cc
+++ b/icing/icing-search-engine_search_test.cc
@@ -19,6 +19,7 @@
 #include <memory>
 #include <string>
 #include <string_view>
+#include <tuple>
 #include <utility>
 #include <vector>
 
@@ -67,13 +68,15 @@
 namespace {
 
 using ::icing::lib::portable_equals_proto::EqualsProto;
+using ::testing::AnyOf;
 using ::testing::DoubleEq;
 using ::testing::DoubleNear;
 using ::testing::ElementsAre;
 using ::testing::Eq;
-using ::testing::Gt;
 using ::testing::HasSubstr;
 using ::testing::IsEmpty;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
 using ::testing::Lt;
 using ::testing::Ne;
 using ::testing::SizeIs;
@@ -137,8 +140,13 @@
   icing_options.set_enable_embedding_index(true);
   icing_options.set_enable_scorable_properties(true);
   icing_options.set_enable_embedding_quantization(true);
+  icing_options.set_enable_repeated_field_joins(true);
+  icing_options.set_enable_soft_index_restoration(true);
   icing_options.set_enable_qualified_id_join_index_v3(true);
-  icing_options.set_enable_delete_propagation_from(false);
+  icing_options.set_enable_delete_propagation_from(true);
+  icing_options.set_enable_passing_filter_to_children(true);
+  icing_options.set_enable_embedding_iterator_v2(true);
+  icing_options.set_enable_non_existent_qualified_id_join(true);
   return icing_options;
 }
 
@@ -936,10 +944,11 @@
       document4;
   SearchResultProto search_result_proto =
       icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
-  EXPECT_THAT(search_result_proto.next_page_token(), Gt(kInvalidNextPageToken));
   uint64_t next_page_token = search_result_proto.next_page_token();
+
   // Since the token is a random number, we don't need to verify
   expected_search_result_proto.set_next_page_token(next_page_token);
+  EXPECT_THAT(search_result_proto.next_page_token(), Ne(kInvalidNextPageToken));
   EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
                                        expected_search_result_proto));
 
@@ -950,6 +959,7 @@
   *expected_search_result_proto.mutable_results()->Add()->mutable_document() =
       document2;
   search_result_proto = icing.GetNextPage(next_page_token);
+  EXPECT_THAT(search_result_proto.next_page_token(), Ne(kInvalidNextPageToken));
   EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
                                        expected_search_result_proto));
 
@@ -961,12 +971,153 @@
   // token.
   expected_search_result_proto.clear_next_page_token();
   search_result_proto = icing.GetNextPage(next_page_token);
+  EXPECT_THAT(search_result_proto.next_page_token(),
+              Eq(kInvalidNextPageToken));  // No more results.
+  EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
+                                       expected_search_result_proto));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       SearchShouldReturnMultiplePages_withMaxResultsLimit) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  // Creates and inserts 5 documents
+  DocumentProto document1 = CreateMessageDocument("namespace", "uri1");
+  DocumentProto document2 = CreateMessageDocument("namespace", "uri2");
+  DocumentProto document3 = CreateMessageDocument("namespace", "uri3");
+  DocumentProto document4 = CreateMessageDocument("namespace", "uri4");
+  DocumentProto document5 = CreateMessageDocument("namespace", "uri5");
+  ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document4).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document5).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::PREFIX);
+  search_spec.set_query("message");
+
+  ResultSpecProto result_spec;
+  result_spec.set_num_per_page(2);
+
+  // Searches and gets the first page, 2 results
+  SearchResultProto expected_search_result_proto;
+  expected_search_result_proto.mutable_status()->set_code(StatusProto::OK);
+  *expected_search_result_proto.mutable_results()->Add()->mutable_document() =
+      document5;
+  *expected_search_result_proto.mutable_results()->Add()->mutable_document() =
+      document4;
+  SearchResultProto search_result_proto =
+      icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
+  uint64_t next_page_token = search_result_proto.next_page_token();
+
+  // Since the token is a random number, we don't need to verify
+  expected_search_result_proto.set_next_page_token(next_page_token);
+  EXPECT_THAT(search_result_proto.next_page_token(), Ne(kInvalidNextPageToken));
   EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
                                        expected_search_result_proto));
 
-  // No more results
+  // Second page, with a max limit of 1 result
   expected_search_result_proto.clear_results();
-  search_result_proto = icing.GetNextPage(next_page_token);
+  *expected_search_result_proto.mutable_results()->Add()->mutable_document() =
+      document3;
+  GetNextPageRequestProto get_next_page_request;
+  get_next_page_request.set_next_page_token(next_page_token);
+  get_next_page_request.set_max_results_to_retrieve_from_page(1);
+  search_result_proto = icing.GetNextPage(std::move(get_next_page_request));
+  EXPECT_THAT(search_result_proto.next_page_token(), Ne(kInvalidNextPageToken));
+  EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
+                                       expected_search_result_proto));
+
+  // Third page, 2 results
+  expected_search_result_proto.clear_results();
+  *expected_search_result_proto.mutable_results()->Add()->mutable_document() =
+      document2;
+  *expected_search_result_proto.mutable_results()->Add()->mutable_document() =
+      document1;
+  get_next_page_request = GetNextPageRequestProto();
+  get_next_page_request.set_next_page_token(next_page_token);
+  search_result_proto = icing.GetNextPage(std::move(get_next_page_request));
+  // Because there are no more results, we should not return the next page
+  // token.
+  expected_search_result_proto.clear_next_page_token();
+  EXPECT_THAT(search_result_proto.next_page_token(),
+              Eq(kInvalidNextPageToken));  // No more results.
+  EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
+                                       expected_search_result_proto));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       SearchShouldReturnMultiplePages_maxResultsLimitLargerThanPageSize) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  // Creates and inserts 5 documents
+  DocumentProto document1 = CreateMessageDocument("namespace", "uri1");
+  DocumentProto document2 = CreateMessageDocument("namespace", "uri2");
+  DocumentProto document3 = CreateMessageDocument("namespace", "uri3");
+  DocumentProto document4 = CreateMessageDocument("namespace", "uri4");
+  DocumentProto document5 = CreateMessageDocument("namespace", "uri5");
+  ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document4).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document5).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::PREFIX);
+  search_spec.set_query("message");
+
+  ResultSpecProto result_spec;
+  result_spec.set_num_per_page(2);
+
+  // Searches and gets the first page -- this should return 2 results
+  SearchResultProto expected_search_result_proto;
+  expected_search_result_proto.mutable_status()->set_code(StatusProto::OK);
+  *expected_search_result_proto.mutable_results()->Add()->mutable_document() =
+      document5;
+  *expected_search_result_proto.mutable_results()->Add()->mutable_document() =
+      document4;
+  SearchResultProto search_result_proto =
+      icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
+  uint64_t next_page_token = search_result_proto.next_page_token();
+
+  // Since the token is a random number, we don't need to verify
+  expected_search_result_proto.set_next_page_token(next_page_token);
+  EXPECT_THAT(search_result_proto.next_page_token(), Ne(kInvalidNextPageToken));
+  EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
+                                       expected_search_result_proto));
+
+  // Second page with a max limit of 5. This should still only return 2
+  // results.
+  expected_search_result_proto.clear_results();
+  *expected_search_result_proto.mutable_results()->Add()->mutable_document() =
+      document3;
+  *expected_search_result_proto.mutable_results()->Add()->mutable_document() =
+      document2;
+  GetNextPageRequestProto get_next_page_request;
+  get_next_page_request.set_next_page_token(next_page_token);
+  get_next_page_request.set_max_results_to_retrieve_from_page(5);
+  search_result_proto = icing.GetNextPage(std::move(get_next_page_request));
+  EXPECT_THAT(search_result_proto.next_page_token(), Ne(kInvalidNextPageToken));
+  EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
+                                       expected_search_result_proto));
+
+  // Third page, only 1 result left.
+  expected_search_result_proto.clear_results();
+  *expected_search_result_proto.mutable_results()->Add()->mutable_document() =
+      document1;
+  get_next_page_request = GetNextPageRequestProto();
+  get_next_page_request.set_next_page_token(next_page_token);
+  search_result_proto = icing.GetNextPage(std::move(get_next_page_request));
+  // Because there are no more results, we should not return the next page
+  // token.
+  expected_search_result_proto.clear_next_page_token();
+  EXPECT_THAT(search_result_proto.next_page_token(),
+              Eq(kInvalidNextPageToken));  // No more results.
   EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
                                        expected_search_result_proto));
 }
@@ -1008,10 +1159,11 @@
       document4;
   SearchResultProto search_result_proto =
       icing.Search(search_spec, scoring_spec, result_spec);
-  EXPECT_THAT(search_result_proto.next_page_token(), Gt(kInvalidNextPageToken));
   uint64_t next_page_token = search_result_proto.next_page_token();
+
   // Since the token is a random number, we don't need to verify
   expected_search_result_proto.set_next_page_token(next_page_token);
+  EXPECT_THAT(search_result_proto.next_page_token(), Ne(kInvalidNextPageToken));
   EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
                                        expected_search_result_proto));
 
@@ -1022,6 +1174,7 @@
   *expected_search_result_proto.mutable_results()->Add()->mutable_document() =
       document2;
   search_result_proto = icing.GetNextPage(next_page_token);
+  EXPECT_THAT(search_result_proto.next_page_token(), Ne(kInvalidNextPageToken));
   EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
                                        expected_search_result_proto));
 
@@ -1033,12 +1186,8 @@
   // token.
   expected_search_result_proto.clear_next_page_token();
   search_result_proto = icing.GetNextPage(next_page_token);
-  EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
-                                       expected_search_result_proto));
-
-  // No more results
-  expected_search_result_proto.clear_results();
-  search_result_proto = icing.GetNextPage(next_page_token);
+  EXPECT_THAT(search_result_proto.next_page_token(),
+              Eq(kInvalidNextPageToken));  // No more results.
   EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
                                        expected_search_result_proto));
 }
@@ -1094,7 +1243,7 @@
       icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
   ASSERT_THAT(search_result.status(), ProtoIsOk());
   ASSERT_THAT(search_result.results(), SizeIs(2));
-  ASSERT_THAT(search_result.next_page_token(), Gt(kInvalidNextPageToken));
+  ASSERT_THAT(search_result.next_page_token(), Ne(kInvalidNextPageToken));
 
   const DocumentProto& document_result_1 = search_result.results(0).document();
   EXPECT_THAT(document_result_1, EqualsProto(document5));
@@ -1123,7 +1272,7 @@
   search_result = icing.GetNextPage(search_result.next_page_token());
   ASSERT_THAT(search_result.status(), ProtoIsOk());
   ASSERT_THAT(search_result.results(), SizeIs(2));
-  ASSERT_THAT(search_result.next_page_token(), Gt(kInvalidNextPageToken));
+  ASSERT_THAT(search_result.next_page_token(), Ne(kInvalidNextPageToken));
 
   const DocumentProto& document_result_3 = search_result.results(0).document();
   EXPECT_THAT(document_result_3, EqualsProto(document3));
@@ -1173,7 +1322,7 @@
       document2;
   SearchResultProto search_result_proto =
       icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
-  EXPECT_THAT(search_result_proto.next_page_token(), Gt(kInvalidNextPageToken));
+  EXPECT_THAT(search_result_proto.next_page_token(), Ne(kInvalidNextPageToken));
   uint64_t next_page_token = search_result_proto.next_page_token();
   // Since the token is a random number, we don't need to verify
   expected_search_result_proto.set_next_page_token(next_page_token);
@@ -1184,9 +1333,12 @@
   // Invalidates token
   icing.InvalidateNextPageToken(next_page_token);
 
-  // Tries to fetch the second page, no result since it's invalidated
+  // Tries to fetch the second page, no result since it's invalidated. Also
+  // page_token_not_found should be set to true in order to hint the client that
+  // the pagination is incomplete.
   expected_search_result_proto.clear_results();
   expected_search_result_proto.clear_next_page_token();
+  expected_search_result_proto.set_page_token_not_found(true);
   search_result_proto = icing.GetNextPage(next_page_token);
   EXPECT_THAT(search_result_proto, EqualsSearchResultIgnoreStatsAndScores(
                                        expected_search_result_proto));
@@ -1277,7 +1429,7 @@
   // Time just has to be greater than the document's creation timestamp (100) +
   // the document's ttl (500)
   auto fake_clock = std::make_unique<FakeClock>();
-  fake_clock->SetSystemTimeMilliseconds(700);
+  FakeClock* fake_clock_ptr = fake_clock.get();
 
   TestIcingSearchEngine icing(GetDefaultIcingOptions(),
                               std::make_unique<Filesystem>(),
@@ -1288,6 +1440,10 @@
   EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
   EXPECT_THAT(icing.Put(document).status(), ProtoIsOk());
 
+  // Adjust the clock to be greater than the document's creation timestamp (100)
+  // + the document's ttl (500).
+  fake_clock_ptr->SetSystemTimeMilliseconds(700);
+
   // Check that the document is not returned as part of search results
   SearchResultProto search_result_proto =
       icing.Search(search_spec, GetDefaultScoringSpec(),
@@ -3910,71 +4066,6 @@
   EXPECT_THAT(results.results(), IsEmpty());
 }
 
-TEST_F(IcingSearchEngineSearchTest, EmptySearchWithPropertyFilter) {
-  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
-  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
-  ASSERT_THAT(icing.SetSchema(CreatePersonAndEmailSchema()).status(),
-              ProtoIsOk());
-
-  // 1. Add two email documents
-  DocumentProto document_one =
-      DocumentBuilder()
-          .SetKey("namespace", "uri1")
-          .SetCreationTimestampMs(1000)
-          .SetSchema("Email")
-          .AddDocumentProperty(
-              "sender",
-              DocumentBuilder()
-                  .SetKey("namespace", "uri1")
-                  .SetSchema("Person")
-                  .AddStringProperty("name", "Meg Ryan")
-                  .AddStringProperty("emailAddress", "hellogirl@aol.com")
-                  .Build())
-          .AddStringProperty("subject", "Hello World!")
-          .AddStringProperty(
-              "body", "Oh what a beautiful morning! Oh what a beautiful day!")
-          .Build();
-  ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk());
-
-  DocumentProto document_two =
-      DocumentBuilder()
-          .SetKey("namespace", "uri2")
-          .SetCreationTimestampMs(1000)
-          .SetSchema("Email")
-          .AddDocumentProperty(
-              "sender", DocumentBuilder()
-                            .SetKey("namespace", "uri2")
-                            .SetSchema("Person")
-                            .AddStringProperty("name", "Tom Hanks")
-                            .AddStringProperty("emailAddress", "ny152@aol.com")
-                            .Build())
-          .AddStringProperty("subject", "Goodnight Moon!")
-          .AddStringProperty("body",
-                             "Count all the sheep and tell them 'Hello'.")
-          .Build();
-  ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk());
-
-  // 2. Issue a query with a property filter
-  auto search_spec = std::make_unique<SearchSpecProto>();
-  search_spec->set_term_match_type(TermMatchType::PREFIX);
-  search_spec->set_query("");
-
-  TypePropertyMask* email_property_filters =
-      search_spec->add_type_property_filters();
-  email_property_filters->set_schema_type("Email");
-  email_property_filters->add_paths("subject");
-
-  auto result_spec = std::make_unique<ResultSpecProto>();
-
-  // 3. Verify that both documents are returned.
-  auto scoring_spec = std::make_unique<ScoringSpecProto>();
-  *scoring_spec = GetDefaultScoringSpec();
-  SearchResultProto results =
-      icing.Search(*search_spec, *scoring_spec, *result_spec);
-  EXPECT_THAT(results.status(), ProtoIsOk());
-  EXPECT_THAT(results.results(), SizeIs(2));
-}
-
 TEST_F(IcingSearchEngineSearchTest, EmptySearchWithEmptyPropertyFilter) {
   IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
@@ -4713,6 +4804,10 @@
   exp_parent_search_stats->set_query_processor_parser_consume_query_latency_ms(
       5);
   exp_parent_search_stats->set_query_processor_query_visitor_latency_ms(5);
+  exp_parent_search_stats->set_num_unquantized_embeddings_scored(0);
+  exp_parent_search_stats->set_num_quantized_embeddings_scored(0);
+  exp_parent_search_stats->set_num_embedding_shards_read(0);
+  exp_parent_search_stats->set_num_embedding_bytes_read(0);
 
   EXPECT_THAT(search_result.query_stats(), EqualsProto(exp_stats));
 
@@ -4720,7 +4815,7 @@
   search_result = icing.GetNextPage(search_result.next_page_token());
   ASSERT_THAT(search_result.status(), ProtoIsOk());
   ASSERT_THAT(search_result.results(), SizeIs(2));
-  ASSERT_THAT(search_result.next_page_token(), Gt(kInvalidNextPageToken));
+  ASSERT_THAT(search_result.next_page_token(), Ne(kInvalidNextPageToken));
 
   exp_stats = QueryStatsProto();
   exp_stats.set_is_first_page(false);
@@ -4731,6 +4826,7 @@
   exp_stats.set_document_retrieval_latency_ms(5);
   exp_stats.set_lock_acquisition_latency_ms(5);
   exp_stats.set_num_joined_results_returned_current_page(0);
+  exp_stats.set_page_token_type(QueryStatsProto::PageTokenType::VALID);
   EXPECT_THAT(search_result.query_stats(), EqualsProto(exp_stats));
 
   // Third page, 1 result with 0 snippets
@@ -4748,6 +4844,75 @@
   exp_stats.set_document_retrieval_latency_ms(5);
   exp_stats.set_lock_acquisition_latency_ms(5);
   exp_stats.set_num_joined_results_returned_current_page(0);
+  exp_stats.set_page_token_type(QueryStatsProto::PageTokenType::VALID);
+  EXPECT_THAT(search_result.query_stats(), EqualsProto(exp_stats));
+
+  // Fetch the next page with kInvalidNextPageToken.
+  search_result = icing.GetNextPage(kInvalidNextPageToken);
+  ASSERT_THAT(search_result.status(), ProtoIsOk());
+  ASSERT_THAT(search_result.results(), IsEmpty());
+  ASSERT_THAT(search_result.next_page_token(), Eq(kInvalidNextPageToken));
+
+  exp_stats = QueryStatsProto();
+  exp_stats.set_is_first_page(false);
+  exp_stats.set_lock_acquisition_latency_ms(5);
+  exp_stats.set_page_token_type(QueryStatsProto::PageTokenType::EMPTY);
+  EXPECT_THAT(search_result.query_stats(), EqualsProto(exp_stats));
+}
+
+TEST_F(IcingSearchEngineSearchTest, GetNextPage_withNotFoundPageToken) {
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetTimerElapsedMilliseconds(5);
+
+  TestIcingSearchEngine icing(GetDefaultIcingOptions(),
+                              std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  // Call GetNextPage with a not found page token.
+  SearchResultProto search_result = icing.GetNextPage(/*next_page_token=*/123);
+  ASSERT_THAT(search_result.status(), ProtoIsOk());
+  ASSERT_THAT(search_result.results(), IsEmpty());
+  ASSERT_THAT(search_result.next_page_token(), Eq(kInvalidNextPageToken));
+  // page_token_not_found is set to true.
+  // - Usually the client uses a valid page token to fetch the next page. If the
+  //   page token is not found, it is "likely" that the corresponding
+  //   ResultState has been removed due to cache eviction.
+  // - If the client really tries to fetch the next page with an non-existing
+  //   page token, then we cannot tell whether the search is "invalid" or
+  //   "incomplete". Still, it is just a hint warning field for the 1st case.
+  EXPECT_THAT(search_result.page_token_not_found(), IsTrue());
+
+  QueryStatsProto exp_stats = QueryStatsProto();
+  exp_stats.set_is_first_page(false);
+  exp_stats.set_lock_acquisition_latency_ms(5);
+  exp_stats.set_page_token_type(QueryStatsProto::PageTokenType::NOT_FOUND);
+  EXPECT_THAT(search_result.query_stats(), EqualsProto(exp_stats));
+}
+
+TEST_F(IcingSearchEngineSearchTest, GetNextPage_withInvalidPageToken) {
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetTimerElapsedMilliseconds(5);
+
+  TestIcingSearchEngine icing(GetDefaultIcingOptions(),
+                              std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  // Call GetNextPage with kInvalidNextPageToken.
+  SearchResultProto search_result = icing.GetNextPage(kInvalidNextPageToken);
+  ASSERT_THAT(search_result.status(), ProtoIsOk());
+  ASSERT_THAT(search_result.results(), IsEmpty());
+  ASSERT_THAT(search_result.next_page_token(), Eq(kInvalidNextPageToken));
+  // page_token_not_found should be false.
+  EXPECT_THAT(search_result.page_token_not_found(), IsFalse());
+
+  QueryStatsProto exp_stats = QueryStatsProto();
+  exp_stats.set_is_first_page(false);
+  exp_stats.set_lock_acquisition_latency_ms(5);
+  exp_stats.set_page_token_type(QueryStatsProto::PageTokenType::EMPTY);
   EXPECT_THAT(search_result.query_stats(), EqualsProto(exp_stats));
 }
 
@@ -4995,6 +5160,10 @@
   exp_parent_search_stats->set_query_processor_parser_consume_query_latency_ms(
       5);
   exp_parent_search_stats->set_query_processor_query_visitor_latency_ms(5);
+  exp_parent_search_stats->set_num_unquantized_embeddings_scored(0);
+  exp_parent_search_stats->set_num_quantized_embeddings_scored(0);
+  exp_parent_search_stats->set_num_embedding_shards_read(0);
+  exp_parent_search_stats->set_num_embedding_bytes_read(0);
 
   QueryStatsProto::SearchStats* exp_child_search_stats =
       exp_stats.mutable_child_search_stats();
@@ -5014,6 +5183,10 @@
   exp_child_search_stats->set_query_processor_parser_consume_query_latency_ms(
       5);
   exp_child_search_stats->set_query_processor_query_visitor_latency_ms(5);
+  exp_child_search_stats->set_num_unquantized_embeddings_scored(0);
+  exp_child_search_stats->set_num_quantized_embeddings_scored(0);
+  exp_child_search_stats->set_num_embedding_shards_read(0);
+  exp_child_search_stats->set_num_embedding_bytes_read(0);
 
   EXPECT_THAT(search_result.query_stats(), EqualsProto(exp_stats));
 
@@ -5034,6 +5207,7 @@
   exp_stats.set_document_retrieval_latency_ms(5);
   exp_stats.set_lock_acquisition_latency_ms(5);
   exp_stats.set_num_joined_results_returned_current_page(1);
+  exp_stats.set_page_token_type(QueryStatsProto::PageTokenType::VALID);
   EXPECT_THAT(search_result.query_stats(), EqualsProto(exp_stats));
 
   // Third page, 0 child docs.
@@ -5052,6 +5226,7 @@
   exp_stats.set_document_retrieval_latency_ms(5);
   exp_stats.set_lock_acquisition_latency_ms(5);
   exp_stats.set_num_results_with_snippets(0);
+  exp_stats.set_page_token_type(QueryStatsProto::PageTokenType::VALID);
   ASSERT_THAT(search_result,
               EqualsSearchResultIgnoreStatsAndScores(expected_result3));
   EXPECT_THAT(search_result.query_stats(), EqualsProto(exp_stats));
@@ -5066,6 +5241,7 @@
   exp_stats = QueryStatsProto();
   exp_stats.set_is_first_page(false);
   exp_stats.set_lock_acquisition_latency_ms(5);
+  exp_stats.set_page_token_type(QueryStatsProto::PageTokenType::EMPTY);
   EXPECT_THAT(search_result.query_stats(), EqualsProto(exp_stats));
 }
 
@@ -5553,6 +5729,361 @@
               EqualsSearchResultIgnoreStatsAndScores(expected_result3));
 }
 
+TEST_F(IcingSearchEngineSearchTest,
+       JoinByQualifiedId_ChildPutBeforeParent_FixDisabled) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("firstName")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("subject")
+                                        .SetDataTypeString(TERM_MATCH_PREFIX,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("personQualifiedId")
+                                        .SetDataTypeJoinableString(
+                                            JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto person =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "person")
+          .SetSchema("Person")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .AddStringProperty("firstName", "first")
+          .Build();
+
+  DocumentProto email =
+      DocumentBuilder()
+          .SetKey("namespace", "email")
+          .SetSchema("Email")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .AddStringProperty("subject", "test subject")
+          .AddStringProperty("personQualifiedId", "pkg$db/namespace#person")
+          .Build();
+
+  IcingSearchEngineOptions icing_options = GetDefaultIcingOptions();
+  icing_options.set_enable_non_existent_qualified_id_join(false);
+  IcingSearchEngine icing(icing_options, GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(person).status(), ProtoIsOk());
+
+  // Parent SearchSpec
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::PREFIX);
+  search_spec.set_query("firstName:first");
+
+  // JoinSpec
+  JoinSpecProto* join_spec = search_spec.mutable_join_spec();
+  join_spec->set_parent_property_expression(
+      std::string(JoinProcessor::kQualifiedIdExpr));
+  join_spec->set_child_property_expression("personQualifiedId");
+  JoinSpecProto::NestedSpecProto* nested_spec =
+      join_spec->mutable_nested_spec();
+  SearchSpecProto* nested_search_spec = nested_spec->mutable_search_spec();
+  nested_search_spec->set_term_match_type(TermMatchType::PREFIX);
+  nested_search_spec->set_query("subject:test");
+
+  *nested_spec->mutable_scoring_spec() = GetDefaultScoringSpec();
+  *nested_spec->mutable_result_spec() = ResultSpecProto::default_instance();
+
+  // Parent ScoringSpec
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+
+  // Parent ResultSpec
+  ResultSpecProto result_spec;
+  result_spec.set_max_joined_children_per_parent_to_return(
+      std::numeric_limits<int32_t>::max());
+
+  // The join relationship should not be established.
+  SearchResultProto expected_result;
+  expected_result.mutable_status()->set_code(StatusProto::OK);
+  SearchResultProto::ResultProto* result_proto =
+      expected_result.mutable_results()->Add();
+  *result_proto->mutable_document() = person;
+
+  EXPECT_THAT(icing.Search(search_spec, scoring_spec, result_spec),
+              EqualsSearchResultIgnoreStatsAndScores(expected_result));
+}
+
+TEST_F(IcingSearchEngineSearchTest, JoinByQualifiedId_ChildPutBeforeParent) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("firstName")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("subject")
+                                        .SetDataTypeString(TERM_MATCH_PREFIX,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("personQualifiedId")
+                                        .SetDataTypeJoinableString(
+                                            JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto person =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "person")
+          .SetSchema("Person")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .AddStringProperty("firstName", "first")
+          .Build();
+
+  DocumentProto email =
+      DocumentBuilder()
+          .SetKey("namespace", "email")
+          .SetSchema("Email")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .AddStringProperty("subject", "test subject")
+          .AddStringProperty("personQualifiedId", "pkg$db/namespace#person")
+          .Build();
+
+  IcingSearchEngineOptions icing_options = GetDefaultIcingOptions();
+  icing_options.set_enable_non_existent_qualified_id_join(true);
+  IcingSearchEngine icing(icing_options, GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(person).status(), ProtoIsOk());
+
+  // Parent SearchSpec
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::PREFIX);
+  search_spec.set_query("firstName:first");
+
+  // JoinSpec
+  JoinSpecProto* join_spec = search_spec.mutable_join_spec();
+  join_spec->set_parent_property_expression(
+      std::string(JoinProcessor::kQualifiedIdExpr));
+  join_spec->set_child_property_expression("personQualifiedId");
+  JoinSpecProto::NestedSpecProto* nested_spec =
+      join_spec->mutable_nested_spec();
+  SearchSpecProto* nested_search_spec = nested_spec->mutable_search_spec();
+  nested_search_spec->set_term_match_type(TermMatchType::PREFIX);
+  nested_search_spec->set_query("subject:test");
+
+  *nested_spec->mutable_scoring_spec() = GetDefaultScoringSpec();
+  *nested_spec->mutable_result_spec() = ResultSpecProto::default_instance();
+
+  // Parent ScoringSpec
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+
+  // Parent ResultSpec
+  ResultSpecProto result_spec;
+  result_spec.set_max_joined_children_per_parent_to_return(
+      std::numeric_limits<int32_t>::max());
+
+  // The join relationship should be established.
+  SearchResultProto expected_result;
+  expected_result.mutable_status()->set_code(StatusProto::OK);
+  SearchResultProto::ResultProto* result_proto =
+      expected_result.mutable_results()->Add();
+  *result_proto->mutable_document() = person;
+  *result_proto->mutable_joined_results()->Add()->mutable_document() = email;
+
+  EXPECT_THAT(icing.Search(search_spec, scoring_spec, result_spec),
+              EqualsSearchResultIgnoreStatsAndScores(expected_result));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       JoinByQualifiedId_ChildPutBeforeParent_DeleteAndPutParentAgain) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("firstName")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("subject")
+                                        .SetDataTypeString(TERM_MATCH_PREFIX,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("personQualifiedId")
+                                        .SetDataTypeJoinableString(
+                                            JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto person =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "person")
+          .SetSchema("Person")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .AddStringProperty("firstName", "first")
+          .Build();
+
+  DocumentProto email =
+      DocumentBuilder()
+          .SetKey("namespace", "email")
+          .SetSchema("Email")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .AddStringProperty("subject", "test subject")
+          .AddStringProperty("personQualifiedId", "pkg$db/namespace#person")
+          .Build();
+
+  IcingSearchEngineOptions icing_options = GetDefaultIcingOptions();
+  IcingSearchEngine icing(icing_options, GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email).status(), ProtoIsOk());
+  // Put, delete and put again the parent document.
+  ASSERT_THAT(icing.Put(person).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Delete(person.namespace_(), person.uri()).status(),
+              ProtoIsOk());
+  ASSERT_THAT(icing.Put(person).status(), ProtoIsOk());
+
+  // Parent SearchSpec
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::PREFIX);
+  search_spec.set_query("firstName:first");
+
+  // JoinSpec
+  JoinSpecProto* join_spec = search_spec.mutable_join_spec();
+  join_spec->set_parent_property_expression(
+      std::string(JoinProcessor::kQualifiedIdExpr));
+  join_spec->set_child_property_expression("personQualifiedId");
+  JoinSpecProto::NestedSpecProto* nested_spec =
+      join_spec->mutable_nested_spec();
+  SearchSpecProto* nested_search_spec = nested_spec->mutable_search_spec();
+  nested_search_spec->set_term_match_type(TermMatchType::PREFIX);
+  nested_search_spec->set_query("subject:test");
+
+  *nested_spec->mutable_scoring_spec() = GetDefaultScoringSpec();
+  *nested_spec->mutable_result_spec() = ResultSpecProto::default_instance();
+
+  // Parent ScoringSpec
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+
+  // Parent ResultSpec
+  ResultSpecProto result_spec;
+  result_spec.set_max_joined_children_per_parent_to_return(
+      std::numeric_limits<int32_t>::max());
+
+  // The join relationship should be established.
+  SearchResultProto expected_result;
+  expected_result.mutable_status()->set_code(StatusProto::OK);
+  SearchResultProto::ResultProto* result_proto =
+      expected_result.mutable_results()->Add();
+  *result_proto->mutable_document() = person;
+  *result_proto->mutable_joined_results()->Add()->mutable_document() = email;
+
+  EXPECT_THAT(icing.Search(search_spec, scoring_spec, result_spec),
+              EqualsSearchResultIgnoreStatsAndScores(expected_result));
+}
+
+TEST_F(
+    IcingSearchEngineSearchTest,
+    JoinByQualifiedId_ChildPutBeforeParent_DeleteAndPutParentAgain_Optimize) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("firstName")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("subject")
+                                        .SetDataTypeString(TERM_MATCH_PREFIX,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("personQualifiedId")
+                                        .SetDataTypeJoinableString(
+                                            JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  DocumentProto person =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "person")
+          .SetSchema("Person")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .AddStringProperty("firstName", "first")
+          .Build();
+
+  DocumentProto email =
+      DocumentBuilder()
+          .SetKey("namespace", "email")
+          .SetSchema("Email")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .AddStringProperty("subject", "test subject")
+          .AddStringProperty("personQualifiedId", "pkg$db/namespace#person")
+          .Build();
+
+  IcingSearchEngineOptions icing_options = GetDefaultIcingOptions();
+  icing_options.set_optimize_rebuild_index_threshold(1.0);
+  IcingSearchEngine icing(icing_options, GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email).status(), ProtoIsOk());
+  // Put, delete, optimize and put again the parent document.
+  ASSERT_THAT(icing.Put(person).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Delete(person.namespace_(), person.uri()).status(),
+              ProtoIsOk());
+  ASSERT_THAT(icing.Optimize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(person).status(), ProtoIsOk());
+
+  // Parent SearchSpec
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::PREFIX);
+  search_spec.set_query("firstName:first");
+
+  // JoinSpec
+  JoinSpecProto* join_spec = search_spec.mutable_join_spec();
+  join_spec->set_parent_property_expression(
+      std::string(JoinProcessor::kQualifiedIdExpr));
+  join_spec->set_child_property_expression("personQualifiedId");
+  JoinSpecProto::NestedSpecProto* nested_spec =
+      join_spec->mutable_nested_spec();
+  SearchSpecProto* nested_search_spec = nested_spec->mutable_search_spec();
+  nested_search_spec->set_term_match_type(TermMatchType::PREFIX);
+  nested_search_spec->set_query("subject:test");
+
+  *nested_spec->mutable_scoring_spec() = GetDefaultScoringSpec();
+  *nested_spec->mutable_result_spec() = ResultSpecProto::default_instance();
+
+  // Parent ScoringSpec
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+
+  // Parent ResultSpec
+  ResultSpecProto result_spec;
+  result_spec.set_max_joined_children_per_parent_to_return(
+      std::numeric_limits<int32_t>::max());
+
+  // The join relationship should be established.
+  SearchResultProto expected_result;
+  expected_result.mutable_status()->set_code(StatusProto::OK);
+  SearchResultProto::ResultProto* result_proto =
+      expected_result.mutable_results()->Add();
+  *result_proto->mutable_document() = person;
+  *result_proto->mutable_joined_results()->Add()->mutable_document() = email;
+
+  EXPECT_THAT(icing.Search(search_spec, scoring_spec, result_spec),
+              EqualsSearchResultIgnoreStatsAndScores(expected_result));
+}
+
 TEST_F(IcingSearchEngineSearchTest, JoinByQualifiedIdMultipleNamespaces) {
   SchemaProto schema =
       SchemaBuilder()
@@ -7255,6 +7786,10 @@
   exp_parent_search_stats->set_query_processor_parser_consume_query_latency_ms(
       5);
   exp_parent_search_stats->set_query_processor_query_visitor_latency_ms(5);
+  exp_parent_search_stats->set_num_unquantized_embeddings_scored(0);
+  exp_parent_search_stats->set_num_quantized_embeddings_scored(0);
+  exp_parent_search_stats->set_num_embedding_shards_read(0);
+  exp_parent_search_stats->set_num_embedding_bytes_read(0);
 
   EXPECT_THAT(results.query_stats(), EqualsProto(exp_stats));
 }
@@ -7745,7 +8280,7 @@
 }
 
 class IcingSearchEngineEmbeddingSearchTest
-    : public ::testing::TestWithParam<bool> {
+    : public ::testing::TestWithParam<std::tuple<bool, bool>> {
  protected:
   void SetUp() override {
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
@@ -7774,6 +8309,9 @@
 };
 
 TEST_P(IcingSearchEngineEmbeddingSearchTest, EmbeddingSearch) {
+  bool get_embedding_match_info = std::get<0>(GetParam());
+  bool enable_eigen_embedding_scoring = std::get<1>(GetParam());
+
   SchemaProto schema =
       SchemaBuilder()
           .AddType(SchemaTypeConfigBuilder()
@@ -7822,7 +8360,9 @@
                              CreateVector("my_model_v2", {0.6, 0.7, -0.8}))
           .Build();
 
-  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_eigen_embedding_scoring(enable_eigen_embedding_scoring);
+  IcingSearchEngine icing(options, GetTestJniCache());
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
   ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
   ASSERT_THAT(icing.Put(document0).status(), ProtoIsOk());
@@ -7850,7 +8390,6 @@
   scoring_spec.set_rank_by(
       ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
 
-  bool get_embedding_match_info = GetParam();
   ResultSpecProto result_spec = ResultSpecProto::default_instance();
   result_spec.mutable_snippet_spec()->set_num_to_snippet(3);
   result_spec.mutable_snippet_spec()->set_num_matches_per_property(5);
@@ -8168,10 +8707,12 @@
   }
 }
 
-INSTANTIATE_TEST_SUITE_P(IcingSearchEngineEmbeddingSearchTest,
-                         IcingSearchEngineEmbeddingSearchTest,
-                         testing::Values(/*enable_embedding_quantization=*/true,
-                                         false));
+INSTANTIATE_TEST_SUITE_P(
+    IcingSearchEngineEmbeddingSearchTest, IcingSearchEngineEmbeddingSearchTest,
+    testing::Values(std::make_tuple(/*get_embedding_match_info=*/false,
+                                    /*enable_eigen_embedding_scoring=*/false),
+                    std::make_tuple(false, true), std::make_tuple(true, false),
+                    std::make_tuple(true, true)));
 
 TEST_F(IcingSearchEngineSearchTest, CannotScoreUnqueriedEmbedding) {
   SchemaProto schema =
@@ -8339,7 +8880,37 @@
               DoubleNear(0 + 0.5, kEps));
 }
 
-TEST_F(IcingSearchEngineSearchTest, EmbeddingSearchWithQuantizedProperty) {
+class IcingSearchEngineEmbeddingSearchQuantizationTest
+    : public ::testing::TestWithParam<bool> {
+ protected:
+  void SetUp() override {
+    if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
+      // If we've specified using the reverse-JNI method for segmentation (i.e.
+      // not ICU), then we won't have the ICU data file included to set up.
+      // Technically, we could choose to use reverse-JNI for segmentation AND
+      // include an ICU data file, but that seems unlikely and our current BUILD
+      // setup doesn't do this.
+      // File generated via icu_data_file rule in //icing/BUILD.
+      std::string icu_data_file_path =
+          GetTestFilePath("icing/icu.dat");
+      ICING_ASSERT_OK(
+          icu_data_file_helper::SetUpIcuDataFile(icu_data_file_path));
+    }
+    filesystem_.CreateDirectoryRecursively(GetTestBaseDir().c_str());
+  }
+
+  void TearDown() override {
+    filesystem_.DeleteDirectoryRecursively(GetTestBaseDir().c_str());
+  }
+
+  const Filesystem* filesystem() const { return &filesystem_; }
+
+ private:
+  Filesystem filesystem_;
+};
+
+TEST_P(IcingSearchEngineEmbeddingSearchQuantizationTest,
+       EmbeddingSearchWithQuantizedProperty) {
   constexpr float eps = 0.0001f;
 
   SchemaProto schema =
@@ -8385,7 +8956,9 @@
           .AddVectorProperty("embeddingQuantized", vector1, vector2)
           .Build();
 
-  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_eigen_embedding_scoring(GetParam());
+  IcingSearchEngine icing(options, GetTestJniCache());
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
   ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
   ASSERT_THAT(icing.Put(document_with_original_embedding).status(),
@@ -8457,6 +9030,11 @@
   EXPECT_THAT(results.results(1).additional_scores(0), DoubleNear(256.45, eps));
 }
 
+INSTANTIATE_TEST_SUITE_P(
+    IcingSearchEngineEmbeddingSearchQuantizationTest,
+    IcingSearchEngineEmbeddingSearchQuantizationTest,
+    testing::Values(/*enable_eigen_embedding_scoring=*/true, false));
+
 TEST_F(IcingSearchEngineSearchTest,
        AdditionalScoresOnlyAllowedInAdvancedScoring) {
   SchemaProto schema =
@@ -8492,6 +9070,216 @@
   EXPECT_THAT(results.status(), ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
 }
 
+TEST_F(IcingSearchEngineSearchTest, EmbeddingSearchStats) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Email1")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("body")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_REPEATED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("embedding")
+                                        .SetDataTypeVector(
+                                            EMBEDDING_INDEXING_LINEAR_SEARCH)
+                                        .SetCardinality(CARDINALITY_REPEATED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("embeddingQuantized")
+                                        .SetDataTypeVector(
+                                            EMBEDDING_INDEXING_LINEAR_SEARCH,
+                                            QUANTIZATION_TYPE_QUANTIZE_8_BIT)
+                                        .SetCardinality(CARDINALITY_REPEATED)))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Email2")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("body")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_REPEATED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("embedding")
+                                        .SetDataTypeVector(
+                                            EMBEDDING_INDEXING_LINEAR_SEARCH)
+                                        .SetCardinality(CARDINALITY_REPEATED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("embeddingQuantized")
+                                        .SetDataTypeVector(
+                                            EMBEDDING_INDEXING_LINEAR_SEARCH,
+                                            QUANTIZATION_TYPE_QUANTIZE_8_BIT)
+                                        .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  // Unquantized size: 12 bytes
+  // Quantized size: 3 + sizeof(Quantizer) = 11 bytes.
+  PropertyProto::VectorProto model1_vector =
+      CreateVector("my_model1", {-1, -1, -1});
+  PropertyProto::VectorProto model2_vector =
+      CreateVector("my_model2", {-1, -1, -1});
+  DocumentProto email1_document =
+      DocumentBuilder()
+          .SetKey("icing", "uri0")
+          .SetSchema("Email1")
+          .SetCreationTimestampMs(1)
+          .AddStringProperty("body", "foo")
+          .AddVectorProperty("embedding", model1_vector, model2_vector)
+          .AddVectorProperty("embeddingQuantized", model1_vector, model2_vector)
+          .Build();
+  DocumentProto email2_document =
+      DocumentBuilder()
+          .SetKey("icing", "uri1")
+          .SetSchema("Email2")
+          .SetCreationTimestampMs(1)
+          .AddStringProperty("body", "foo")
+          .AddVectorProperty("embedding", model1_vector, model2_vector)
+          .AddVectorProperty("embeddingQuantized", model1_vector, model2_vector)
+          .Build();
+
+  IcingSearchEngineOptions icing_options = GetDefaultIcingOptions();
+  // Set the number of shards to a large number to avoid hash collisions.
+  icing_options.set_embedding_index_num_shards(8192);
+  IcingSearchEngine icing(icing_options, GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email1_document).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email2_document).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::EXACT_ONLY);
+  search_spec.set_embedding_query_metric_type(
+      SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT);
+  search_spec.add_enabled_features(
+      std::string(kListFilterQueryLanguageFeature));
+  *search_spec.add_embedding_query_vectors() = model1_vector;
+  *search_spec.add_embedding_query_vectors() = model2_vector;
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+
+  // Perform an embedding query that matches all embeddings.
+  search_spec.set_query(
+      "semanticSearch(getEmbeddingParameter(0)) OR "
+      "semanticSearch(getEmbeddingParameter(1))");
+  SearchResultProto results = icing.Search(search_spec, scoring_spec,
+                                           ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(email2_document));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(email1_document));
+  // Check embedding search stats.
+  QueryStatsProto::SearchStats search_stats =
+      results.query_stats().parent_search_stats();
+  // We have 8 embeddings in total in the two documents. 4 of them are
+  // unquantized and 4 of them are quantized.
+  EXPECT_THAT(search_stats.num_unquantized_embeddings_scored(), Eq(4));
+  EXPECT_THAT(search_stats.num_quantized_embeddings_scored(), Eq(4));
+  // We have 2 types and 2 models, which corresponds to 4 shards for unquantized
+  // embeddings and 4 shards for quantized embeddings.
+  EXPECT_THAT(search_stats.num_embedding_shards_read(), Eq(8));
+  // 4 unquantized embeddings * 12 bytes + 4 quantized embeddings * 11 bytes.
+  EXPECT_THAT(search_stats.num_embedding_bytes_read(), Eq(92));
+
+  // Perform an embedding query that only matches model1 embeddings.
+  search_spec.set_query("semanticSearch(getEmbeddingParameter(0))");
+  results = icing.Search(search_spec, scoring_spec,
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(email2_document));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(email1_document));
+  // Check embedding search stats.
+  search_stats = results.query_stats().parent_search_stats();
+  // We have 4 embeddings in total in the two documents. 2 of them are
+  // unquantized and 2 of them are quantized.
+  EXPECT_THAT(search_stats.num_unquantized_embeddings_scored(), Eq(2));
+  EXPECT_THAT(search_stats.num_quantized_embeddings_scored(), Eq(2));
+  // We have 2 types and 1 models matched, which corresponds to 2 shards for
+  // unquantized embeddings and 2 shards for quantized embeddings.
+  EXPECT_THAT(search_stats.num_embedding_shards_read(), Eq(4));
+  // 2 unquantized embeddings * 12 bytes + 2 quantized embeddings * 11 bytes.
+  EXPECT_THAT(search_stats.num_embedding_bytes_read(), Eq(46));
+
+  // Perform an embedding query that only matches model1 embeddings and filter
+  // on Email1.
+  search_spec.set_query("semanticSearch(getEmbeddingParameter(0))");
+  search_spec.add_schema_type_filters("Email1");
+  results = icing.Search(search_spec, scoring_spec,
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(1));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(email1_document));
+  // Check embedding search stats.
+  search_stats = results.query_stats().parent_search_stats();
+  // We have 2 embeddings in total in the Email1 document. 1 of them are
+  // unquantized and 1 of them are quantized.
+  EXPECT_THAT(search_stats.num_unquantized_embeddings_scored(), Eq(1));
+  EXPECT_THAT(search_stats.num_quantized_embeddings_scored(), Eq(1));
+  // We have 1 types and 1 models matched, which corresponds to 1 shards for
+  // unquantized embeddings and 1 shards for quantized embeddings.
+  EXPECT_THAT(search_stats.num_embedding_shards_read(), Eq(2));
+  // 1 unquantized embeddings * 12 bytes + 1 quantized embeddings * 11 bytes.
+  EXPECT_THAT(search_stats.num_embedding_bytes_read(), Eq(23));
+
+  // Now add a property filter on Email1.embeddingQuantized, which will only
+  // score quantized embeddings in Email1.
+  TypePropertyMask* property_filters = search_spec.add_type_property_filters();
+  property_filters->set_schema_type("Email1");
+  property_filters->add_paths("embeddingQuantized");
+  results = icing.Search(search_spec, scoring_spec,
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(1));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(email1_document));
+  // Check embedding search stats.
+  search_stats = results.query_stats().parent_search_stats();
+  // We have 1 quantized embeddings in the Email1 document.
+  EXPECT_THAT(search_stats.num_unquantized_embeddings_scored(), Eq(0));
+  EXPECT_THAT(search_stats.num_quantized_embeddings_scored(), Eq(1));
+  // We have 1 types and 1 models matched, which corresponds to 0 shard for
+  // unquantized embeddings and 1 shard for quantized embeddings.
+  EXPECT_THAT(search_stats.num_embedding_shards_read(), Eq(1));
+  // 1 quantized embeddings * 11 bytes.
+  EXPECT_THAT(search_stats.num_embedding_bytes_read(), Eq(11));
+}
+
+TEST_F(IcingSearchEngineSearchTest, EmbeddingSearchStats_EmptyIndex) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Email").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("embedding")
+                  .SetDataTypeVector(EMBEDDING_INDEXING_LINEAR_SEARCH)
+                  .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+
+  IcingSearchEngineOptions icing_options = GetDefaultIcingOptions();
+  IcingSearchEngine icing(icing_options, GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::EXACT_ONLY);
+  search_spec.set_embedding_query_metric_type(
+      SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT);
+  search_spec.add_enabled_features(
+      std::string(kListFilterQueryLanguageFeature));
+  *search_spec.add_embedding_query_vectors() =
+      CreateVector("my_model", {-1, -1, -1});
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+
+  // Perform an embedding query.
+  search_spec.set_query("semanticSearch(getEmbeddingParameter(0))");
+  SearchResultProto results = icing.Search(search_spec, scoring_spec,
+                                           ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), IsEmpty());
+  // Check embedding search stats.
+  QueryStatsProto::SearchStats search_stats =
+      results.query_stats().parent_search_stats();
+  EXPECT_THAT(search_stats.num_unquantized_embeddings_scored(), Eq(0));
+  EXPECT_THAT(search_stats.num_quantized_embeddings_scored(), Eq(0));
+  EXPECT_THAT(search_stats.num_embedding_shards_read(), Eq(0));
+  EXPECT_THAT(search_stats.num_embedding_bytes_read(), Eq(0));
+}
+
 TEST_F(IcingSearchEngineSearchTest,
        EmbeddingSearchWithManyFilteredOutDocuments) {
   SchemaProto schema =
@@ -8659,6 +9447,213 @@
   EXPECT_THAT(results.results(), IsEmpty());
 }
 
+TEST_F(IcingSearchEngineSearchTest, SearchWithTypeFiltersEmbedding) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("TypeA").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("embedding")
+                  .SetDataTypeVector(EMBEDDING_INDEXING_LINEAR_SEARCH)
+                  .SetCardinality(CARDINALITY_REPEATED)))
+          .AddType(SchemaTypeConfigBuilder().SetType("TypeB").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("embedding")
+                  .SetDataTypeVector(EMBEDDING_INDEXING_LINEAR_SEARCH)
+                  .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+
+  DocumentProto doc_a =
+      DocumentBuilder()
+          .SetKey("icing", "uriA")
+          .SetSchema("TypeA")
+          .SetCreationTimestampMs(1)
+          .AddVectorProperty("embedding",
+                             CreateVector("my_model", {0.1, 0.2, 0.3}))
+          .Build();
+  DocumentProto doc_b =
+      DocumentBuilder()
+          .SetKey("icing", "uriB")
+          .SetSchema("TypeB")
+          .SetCreationTimestampMs(1)
+          .AddVectorProperty("embedding",
+                             CreateVector("my_model", {0.4, 0.5, 0.6}))
+          .Build();
+
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(doc_a).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(doc_b).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  search_spec.set_embedding_query_metric_type(
+      SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT);
+  search_spec.add_enabled_features(
+      std::string(kListFilterQueryLanguageFeature));
+  *search_spec.add_embedding_query_vectors() =
+      CreateVector("my_model", {1, 1, 1});
+  search_spec.set_query("semanticSearch(getEmbeddingParameter(0))");
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+  scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  scoring_spec.set_advanced_scoring_expression(
+      "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))");
+
+  // Filter for TypeA only
+  search_spec.add_schema_type_filters("TypeA");
+  SearchResultProto results = icing.Search(search_spec, scoring_spec,
+                                           ResultSpecProto::default_instance());
+  // Verify that only doc_a is returned due to the schema type filter.
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  ASSERT_THAT(results.results(), SizeIs(1));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(doc_a));
+  // Score should be 0.1 + 0.2 + 0.3 = 0.6
+  EXPECT_THAT(results.results(0).score(), DoubleNear(0.6, kEps));
+
+  // Filter for TypeB only
+  search_spec.clear_schema_type_filters();
+  search_spec.add_schema_type_filters("TypeB");
+  results = icing.Search(search_spec, scoring_spec,
+                         ResultSpecProto::default_instance());
+  // Verify that only doc_b is returned due to the schema type filter.
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  ASSERT_THAT(results.results(), SizeIs(1));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(doc_b));
+  // Score should be 0.4 + 0.5 + 0.6 = 1.5
+  EXPECT_THAT(results.results(0).score(), DoubleNear(1.5, kEps));
+}
+
+TEST_F(IcingSearchEngineSearchTest, SearchWithTypeFiltersEmbedding_manyHits) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("TypeA").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("embedding")
+                  .SetDataTypeVector(EMBEDDING_INDEXING_LINEAR_SEARCH)
+                  .SetCardinality(CARDINALITY_REPEATED)))
+          .AddType(SchemaTypeConfigBuilder().SetType("TypeB").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("embedding")
+                  .SetDataTypeVector(EMBEDDING_INDEXING_LINEAR_SEARCH)
+                  .SetCardinality(CARDINALITY_REPEATED)))
+          .AddType(SchemaTypeConfigBuilder().SetType("TypeC").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("embedding")
+                  .SetDataTypeVector(EMBEDDING_INDEXING_LINEAR_SEARCH)
+                  .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+
+  // Add 600 documents of TypeA, each of which has two embedding hits.
+  for (int i = 0; i < 600; ++i) {
+    DocumentProto doc =
+        DocumentBuilder()
+            .SetKey("icing", "uriA" + std::to_string(i))
+            .SetSchema("TypeA")
+            .SetCreationTimestampMs(1)
+            .AddVectorProperty("embedding",
+                               CreateVector("my_model", {0.1, 0.2, 0.3}),
+                               CreateVector("my_model", {0.1, 0.2, 0.3}))
+            .Build();
+    ASSERT_THAT(icing.Put(doc).status(), ProtoIsOk());
+  }
+  // Add 600 documents of TypeB, each of which has two embedding hits.
+  for (int i = 0; i < 600; ++i) {
+    DocumentProto doc =
+        DocumentBuilder()
+            .SetKey("icing", "uriB" + std::to_string(i))
+            .SetSchema("TypeB")
+            .SetCreationTimestampMs(1)
+            .AddVectorProperty("embedding",
+                               CreateVector("my_model", {0.4, 0.5, 0.6}),
+                               CreateVector("my_model", {0.4, 0.5, 0.6}))
+            .Build();
+    ASSERT_THAT(icing.Put(doc).status(), ProtoIsOk());
+  }
+  // Add 600 documents of TypeC, each of which has two embedding hits.
+  for (int i = 0; i < 600; ++i) {
+    DocumentProto doc =
+        DocumentBuilder()
+            .SetKey("icing", "uriC" + std::to_string(i))
+            .SetSchema("TypeC")
+            .SetCreationTimestampMs(1)
+            .AddVectorProperty("embedding",
+                               CreateVector("my_model", {0.7, 0.8, 0.9}),
+                               CreateVector("my_model", {0.7, 0.8, 0.9}))
+            .Build();
+    ASSERT_THAT(icing.Put(doc).status(), ProtoIsOk());
+  }
+
+  SearchSpecProto search_spec;
+  search_spec.set_embedding_query_metric_type(
+      SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT);
+  search_spec.add_enabled_features(
+      std::string(kListFilterQueryLanguageFeature));
+  *search_spec.add_embedding_query_vectors() =
+      CreateVector("my_model", {1, 1, 1});
+  search_spec.set_query("semanticSearch(getEmbeddingParameter(0))");
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+  scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  // Let the score be the number of embedding hits.
+  scoring_spec.set_advanced_scoring_expression(
+      "len(this.matchedSemanticScores(getEmbeddingParameter(0)))");
+  ResultSpecProto result_spec;
+  result_spec.set_num_per_page(10000);
+
+  // Filter for TypeA only
+  search_spec.add_schema_type_filters("TypeA");
+  SearchResultProto results =
+      icing.Search(search_spec, scoring_spec, result_spec);
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  ASSERT_THAT(results.results(), SizeIs(600));
+  for (int i = 0; i < 600; ++i) {
+    EXPECT_THAT(results.results(i).document().schema(), Eq("TypeA"));
+    // Each document has 2 embedding hits.
+    EXPECT_THAT(results.results(i).score(), Eq(2));
+  }
+
+  // Filter for TypeB only
+  search_spec.clear_schema_type_filters();
+  search_spec.add_schema_type_filters("TypeB");
+  results = icing.Search(search_spec, scoring_spec, result_spec);
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  ASSERT_THAT(results.results(), SizeIs(600));
+  for (int i = 0; i < 600; ++i) {
+    EXPECT_THAT(results.results(i).document().schema(), Eq("TypeB"));
+    // Each document has 2 embedding hits.
+    EXPECT_THAT(results.results(i).score(), Eq(2));
+  }
+
+  // Filter for TypeC only
+  search_spec.clear_schema_type_filters();
+  search_spec.add_schema_type_filters("TypeC");
+  results = icing.Search(search_spec, scoring_spec, result_spec);
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  ASSERT_THAT(results.results(), SizeIs(600));
+  for (int i = 0; i < 600; ++i) {
+    EXPECT_THAT(results.results(i).document().schema(), Eq("TypeC"));
+    // Each document has 2 embedding hits.
+    EXPECT_THAT(results.results(i).score(), Eq(2));
+  }
+
+  // Filter for TypeA and TypeC
+  search_spec.clear_schema_type_filters();
+  search_spec.add_schema_type_filters("TypeA");
+  search_spec.add_schema_type_filters("TypeC");
+  results = icing.Search(search_spec, scoring_spec, result_spec);
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  ASSERT_THAT(results.results(), SizeIs(1200));
+  for (int i = 0; i < 1200; ++i) {
+    EXPECT_THAT(results.results(i).document().schema(),
+                AnyOf(Eq("TypeA"), Eq("TypeC")));
+    // Each document has 2 embedding hits.
+    EXPECT_THAT(results.results(i).score(), Eq(2));
+  }
+}
+
 TEST_F(IcingSearchEngineSearchTest, SearchWithUriFilters) {
   IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
@@ -8775,62 +9770,6 @@
               HasSubstr("does not appear in the namespace filter"));
 }
 
-TEST_F(IcingSearchEngineSearchTest, UriFiltersWorkWithPropertyFilters) {
-  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
-  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
-  ASSERT_THAT(icing.SetSchema(CreateEmailSchema()).status(), ProtoIsOk());
-
-  // Add three documents with different URIs.
-  DocumentProto document_one =
-      DocumentBuilder()
-          .SetKey("namespace", "uri1")
-          .SetSchema("Email")
-          .AddStringProperty("subject", "foo")
-          .AddStringProperty("body", "foo")
-          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
-          .Build();
-  ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk());
-  DocumentProto document_two =
-      DocumentBuilder()
-          .SetKey("namespace", "uri2")
-          .SetSchema("Email")
-          .AddStringProperty("subject", "foo")
-          .AddStringProperty("body", "foo")
-          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
-          .Build();
-  ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk());
-  DocumentProto document_three =
-      DocumentBuilder()
-          .SetKey("namespace", "uri3")
-          .SetSchema("Email")
-          .AddStringProperty("subject", "baz")
-          .AddStringProperty("body", "foo")
-          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
-          .Build();
-  ASSERT_THAT(icing.Put(document_three).status(), ProtoIsOk());
-
-  // Create a search spec with uri filters that should only match document1 and
-  // document3, and a property filter that only searches for "subject". As a
-  // result, only document1 should be returned.
-  SearchSpecProto search_spec;
-  search_spec.set_term_match_type(TermMatchType::PREFIX);
-  search_spec.set_query("foo");
-  NamespaceDocumentUriGroup* uris = search_spec.add_document_uri_filters();
-  uris->set_namespace_("namespace");
-  uris->add_document_uris("uri1");
-  uris->add_document_uris("uri3");
-  TypePropertyMask* property_filters = search_spec.add_type_property_filters();
-  property_filters->set_schema_type("Email");
-  property_filters->add_paths("subject");
-
-  // Check results
-  SearchResultProto results = icing.Search(search_spec, GetDefaultScoringSpec(),
-                                           ResultSpecProto::default_instance());
-  EXPECT_THAT(results.status(), ProtoIsOk());
-  EXPECT_THAT(results.results(), SizeIs(1));
-  EXPECT_THAT(results.results(0).document(), EqualsProto(document_one));
-}
-
 TEST_F(IcingSearchEngineSearchTest, SearchWithRankingByScorableProperty) {
   SchemaProto schema =
       SchemaBuilder()
@@ -9692,7 +10631,20 @@
                           .SetScorableType(SCORABLE_TYPE_ENABLED)
                           .SetCardinality(CARDINALITY_REPEATED)))
           .Build();
-  EXPECT_THAT(icing.SetSchema(new_schema).status(), ProtoIsOk());
+  SetSchemaResultProto set_schema_result = icing.SetSchema(new_schema);
+  // Ignore latency numbers as they're covered elsewhere
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
+  SetSchemaResultProto expected_set_schema_result = SetSchemaResultProto();
+  expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result
+      .mutable_scorable_property_incompatible_changed_schema_types()
+      ->Add("Person");
+  // Since it is a scorable property incompatible change, we need to do a
+  // recovery proof flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
+  EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   actual_search_result_proto = icing.Search(
       search_spec, scoring_spec, ResultSpecProto::default_instance());
@@ -9767,7 +10719,20 @@
                           .SetScorableType(SCORABLE_TYPE_DISABLED)
                           .SetCardinality(CARDINALITY_REPEATED)))
           .Build();
-  EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  SetSchemaResultProto set_schema_result = icing.SetSchema(schema);
+  // Ignore latency numbers as they're covered elsewhere
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
+  SetSchemaResultProto expected_set_schema_result = SetSchemaResultProto();
+  expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result
+      .mutable_scorable_property_incompatible_changed_schema_types()
+      ->Add("Person");
+  // Since it is a scorable property incompatible change, we need to do a
+  // recovery proof flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
+  EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   SearchResultProto search_result_proto = icing.Search(
       search_spec, scoring_spec, ResultSpecProto::default_instance());
@@ -9859,7 +10824,20 @@
                           .SetScorableType(SCORABLE_TYPE_ENABLED)
                           .SetCardinality(CARDINALITY_REPEATED)))
           .Build();
-  EXPECT_THAT(icing.SetSchema(new_schema).status(), ProtoIsOk());
+  SetSchemaResultProto set_schema_result = icing.SetSchema(new_schema);
+  // Ignore latency numbers as they're covered elsewhere
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
+  SetSchemaResultProto expected_set_schema_result = SetSchemaResultProto();
+  expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result
+      .mutable_scorable_property_incompatible_changed_schema_types()
+      ->Add("Person");
+  // Since it is a scorable property incompatible change, we need to do a
+  // recovery proof flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
+  EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   SearchSpecProto search_spec;
   ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
@@ -9956,7 +10934,14 @@
                           .SetScorableType(SCORABLE_TYPE_ENABLED)
                           .SetCardinality(CARDINALITY_REPEATED)))
           .Build();
-  EXPECT_THAT(icing.SetSchema(new_schema).status(), ProtoIsOk());
+  SetSchemaResultProto set_schema_result = icing.SetSchema(new_schema);
+  EXPECT_THAT(set_schema_result.status(), ProtoIsOk());
+  EXPECT_THAT(
+      set_schema_result.scorable_property_incompatible_changed_schema_types(),
+      IsEmpty());
+  // Flush is not needed since the schema change is (scorable property)
+  // compatible.
+  EXPECT_THAT(set_schema_result.needs_persist_type(), PersistType::UNKNOWN);
 
   SearchSpecProto search_spec;
   ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
@@ -10086,7 +11071,23 @@
                                    "Person", /*index_nested_properties=*/true)
                                .SetCardinality(CARDINALITY_REPEATED)))
           .Build();
-  EXPECT_THAT(icing.SetSchema(schema_proto).status(), ProtoIsOk());
+  SetSchemaResultProto set_schema_result = icing.SetSchema(schema_proto);
+  // Ignore latency numbers as they're covered elsewhere
+  set_schema_result.clear_set_schema_stats();
+  set_schema_result.clear_vm_binder_transaction_latency_start_time_ms();
+  SetSchemaResultProto expected_set_schema_result = SetSchemaResultProto();
+  expected_set_schema_result.mutable_status()->set_code(StatusProto::OK);
+  expected_set_schema_result
+      .mutable_scorable_property_incompatible_changed_schema_types()
+      ->Add("Email");
+  expected_set_schema_result
+      .mutable_scorable_property_incompatible_changed_schema_types()
+      ->Add("Person");
+  // Since it is a scorable property incompatible change, we need to do a
+  // recovery proof flush.
+  expected_set_schema_result.set_needs_persist_type(
+      PersistType::RECOVERY_PROOF);
+  EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
 
   SearchSpecProto search_spec;
   ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
@@ -10570,6 +11571,494 @@
   EXPECT_THAT(results.results(1).joined_results(), IsEmpty());
 }
 
+// Section restriction should not be applicable to
+// DocHitInfoIteratorAllDocumentId. Otherwise, no document will be returned,
+// since the iterator does not have any section information.
+TEST_F(IcingSearchEngineSearchTest, PropertyRestrictionWorksWithEmptySearch) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreatePersonAndEmailSchema()).status(),
+              ProtoIsOk());
+
+  // 1. Add two email documents
+  DocumentProto document_one =
+      DocumentBuilder()
+          .SetKey("namespace", "uri1")
+          .SetCreationTimestampMs(1000)
+          .SetSchema("Email")
+          .AddDocumentProperty(
+              "sender",
+              DocumentBuilder()
+                  .SetKey("namespace", "uri1")
+                  .SetSchema("Person")
+                  .AddStringProperty("name", "Meg Ryan")
+                  .AddStringProperty("emailAddress", "hellogirl@aol.com")
+                  .Build())
+          .AddStringProperty("subject", "Hello World!")
+          .AddStringProperty(
+              "body", "Oh what a beautiful morning! Oh what a beautiful day!")
+          .Build();
+  ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk());
+
+  DocumentProto document_two =
+      DocumentBuilder()
+          .SetKey("namespace", "uri2")
+          .SetCreationTimestampMs(1000)
+          .SetSchema("Email")
+          .AddDocumentProperty(
+              "sender", DocumentBuilder()
+                            .SetKey("namespace", "uri2")
+                            .SetSchema("Person")
+                            .AddStringProperty("name", "Tom Hanks")
+                            .AddStringProperty("emailAddress", "ny152@aol.com")
+                            .Build())
+          .AddStringProperty("subject", "Goodnight Moon!")
+          .AddStringProperty("body",
+                             "Count all the sheep and tell them 'Hello'.")
+          .Build();
+  ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk());
+
+  // 2. Issue a query with a property filter
+  auto search_spec = std::make_unique<SearchSpecProto>();
+  search_spec->set_term_match_type(TermMatchType::PREFIX);
+  search_spec->set_query("");
+
+  TypePropertyMask* email_property_filters =
+      search_spec->add_type_property_filters();
+  email_property_filters->set_schema_type("Email");
+  email_property_filters->add_paths("subject");
+
+  auto result_spec = std::make_unique<ResultSpecProto>();
+
+  // 3. Verify that both documents are returned.
+  auto scoring_spec = std::make_unique<ScoringSpecProto>();
+  *scoring_spec = GetDefaultScoringSpec();
+  SearchResultProto results =
+      icing.Search(*search_spec, *scoring_spec, *result_spec);
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+}
+
+// Section restriction should not be applicable to DocHitInfoIteratorByUri.
+// Otherwise, no document will be returned, since the iterator does not have any
+// section information.
+TEST_F(IcingSearchEngineSearchTest, PropertyRestrictionWorksWithUriFilters) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreateEmailSchema()).status(), ProtoIsOk());
+
+  // Add three documents with different URIs.
+  DocumentProto document_one =
+      DocumentBuilder()
+          .SetKey("namespace", "uri1")
+          .SetSchema("Email")
+          .AddStringProperty("subject", "foo")
+          .AddStringProperty("body", "foo")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk());
+  DocumentProto document_two =
+      DocumentBuilder()
+          .SetKey("namespace", "uri2")
+          .SetSchema("Email")
+          .AddStringProperty("subject", "foo")
+          .AddStringProperty("body", "foo")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk());
+  DocumentProto document_three =
+      DocumentBuilder()
+          .SetKey("namespace", "uri3")
+          .SetSchema("Email")
+          .AddStringProperty("subject", "baz")
+          .AddStringProperty("body", "foo")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  ASSERT_THAT(icing.Put(document_three).status(), ProtoIsOk());
+
+  // Create a search spec with uri filters that should only match document1 and
+  // document3, and a property filter that only searches for "foo" in "subject".
+  // As a result, only document1 should be returned.
+  SearchSpecProto search_spec1;
+  search_spec1.set_term_match_type(TermMatchType::PREFIX);
+  search_spec1.set_query("foo");
+  NamespaceDocumentUriGroup* uris = search_spec1.add_document_uri_filters();
+  uris->set_namespace_("namespace");
+  uris->add_document_uris("uri1");
+  uris->add_document_uris("uri3");
+  TypePropertyMask* property_filters = search_spec1.add_type_property_filters();
+  property_filters->set_schema_type("Email");
+  property_filters->add_paths("subject");
+  SearchResultProto results =
+      icing.Search(search_spec1, GetDefaultScoringSpec(),
+                   ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(1));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document_one));
+
+  // Same as above with property filter in the query expression.
+  SearchSpecProto search_spec2;
+  search_spec2.set_term_match_type(TermMatchType::PREFIX);
+  search_spec2.set_query("subject:foo");
+  uris = search_spec2.add_document_uri_filters();
+  uris->set_namespace_("namespace");
+  uris->add_document_uris("uri1");
+  uris->add_document_uris("uri3");
+  results = icing.Search(search_spec2, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(1));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document_one));
+
+  // For empty query, property filter is not applicable, since there is no
+  // terms. For this case, only uri filters are applied.
+  SearchSpecProto search_spec3;
+  search_spec3.set_term_match_type(TermMatchType::PREFIX);
+  search_spec3.set_query("");
+  uris = search_spec3.add_document_uri_filters();
+  uris->set_namespace_("namespace");
+  uris->add_document_uris("uri1");
+  uris->add_document_uris("uri3");
+  property_filters = search_spec3.add_type_property_filters();
+  property_filters->set_schema_type("Email");
+  // This property filter should be ignored.
+  property_filters->add_paths("non_existent_property");
+  results = icing.Search(search_spec3, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document_three));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(document_one));
+}
+
+// Section restriction should not be applicable to
+// DocHitInfoIteratorMatchScoreExpression. Otherwise, no document will be
+// returned, since the iterator does not have any section information.
+TEST_F(IcingSearchEngineSearchTest,
+       PropertyRestrictionWorksWithMatchScoreExpression) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Message")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("title")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_REPEATED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("body")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+
+  // Add three documents with different document scores.
+  DocumentProto document1 =
+      DocumentBuilder()
+          .SetKey("namespace", "uri1")
+          .SetSchema("Message")
+          .AddStringProperty("body", "foo")
+          .SetScore(2)
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto document2 =
+      DocumentBuilder()
+          .SetKey("namespace", "uri2")
+          .SetSchema("Message")
+          .AddStringProperty("title", "foo")
+          .SetScore(3)
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto document3 =
+      DocumentBuilder()
+          .SetKey("namespace", "uri3")
+          .SetSchema("Message")
+          .AddStringProperty("body", "foo")
+          .SetScore(4)
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+
+  ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document3).status(), ProtoIsOk());
+
+  // Get documents with a document score in [3, 4], which matches document 2
+  // and 3.
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::EXACT_ONLY);
+  search_spec.add_enabled_features(
+      std::string(kMatchScoreExpressionFunctionFeature));
+  search_spec.add_enabled_features(
+      std::string(kListFilterQueryLanguageFeature));
+  search_spec.set_query("matchScoreExpression(\"this.documentScore()\", 3, 4)");
+  SearchResultProto results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                                           ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document3));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(document2));
+
+  // Check that the property restriction does not apply to matchScoreExpression.
+  search_spec.set_query(
+      "body:matchScoreExpression(\"this.documentScore()\", 3, 4)");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document3));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(document2));
+
+  // Same as above.
+  search_spec.set_query(
+      "non_existent_property:"
+      "matchScoreExpression(\"this.documentScore()\", 3, 4)");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document3));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(document2));
+
+  // Property restriction works as normal for "foo" in "body", so document 2
+  // will not be returned.
+  search_spec.set_query(
+      "body:(foo AND matchScoreExpression(\"this.documentScore()\", 3, 4))");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(1));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document3));
+
+  // Property restriction works as normal for "foo" in "non_existent_property",
+  // which should return no results.
+  search_spec.set_query(
+      "non_existent_property:(foo AND "
+      "matchScoreExpression(\"this.documentScore()\", 3, 4))");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), IsEmpty());
+}
+
+// Section restriction should not be applicable to
+// DocHitInfoIteratorPropertyInDocument. Otherwise, no document will be
+// returned, since the iterator does not have any section information.
+TEST_F(IcingSearchEngineSearchTest, PropertyRestrictionWorksWithHasProperty) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Value")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("title")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_REPEATED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("body")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+
+  // A document with every property.
+  DocumentProto document0 = DocumentBuilder()
+                                .SetKey("icing", "uri0")
+                                .SetSchema("Value")
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("title", "foo")
+                                .AddStringProperty("body", "foo")
+                                .Build();
+  // A document with missing body.
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("icing", "uri1")
+                                .SetSchema("Value")
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("title", "bar")
+                                .Build();
+  // A document with missing title.
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("icing", "uri2")
+                                .SetSchema("Value")
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("body", "bar")
+                                .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_build_property_existence_metadata_hits(true);
+  IcingSearchEngine icing(options, GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document0).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk());
+
+  // Get all documents that have "body".
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::EXACT_ONLY);
+
+  search_spec.add_enabled_features(std::string(kHasPropertyFunctionFeature));
+  search_spec.add_enabled_features(
+      std::string(kListFilterQueryLanguageFeature));
+  search_spec.set_query("hasProperty(\"body\")");
+  SearchResultProto results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                                           ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document2));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(document0));
+
+  // Check that the property restriction does not apply to hasProperty.
+  search_spec.set_query("body:hasProperty(\"body\")");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document2));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(document0));
+
+  // Same as above.
+  search_spec.set_query("title:hasProperty(\"body\")");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document2));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(document0));
+
+  // Same as above.
+  search_spec.set_query("non_existent_property:hasProperty(\"body\")");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document2));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(document0));
+
+  // Property restriction works as normal for "foo" in "body".
+  search_spec.set_query("body:(foo AND hasProperty(\"body\"))");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(1));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document0));
+
+  // Property restriction works as normal for "foo" in "non_existent_property",
+  // which should return no results.
+  search_spec.set_query(
+      "non_existent_property:(foo AND hasProperty(\"body\"))");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), IsEmpty());
+}
+
+// Section restriction should not be applicable to
+// DocHitInfoIteratorPropertyInSchema. Otherwise, no document will be returned,
+// since the iterator does not have any section information.
+TEST_F(IcingSearchEngineSearchTest,
+       PropertyRestrictionWorksWithPropertyDefined) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Value")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("title")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_REPEATED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("body")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+
+  // Create documents.
+  DocumentProto document0 = DocumentBuilder()
+                                .SetKey("icing", "uri0")
+                                .SetSchema("Value")
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("title", "foo")
+                                .AddStringProperty("body", "foo")
+                                .Build();
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("icing", "uri1")
+                                .SetSchema("Value")
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("title", "bar")
+                                .AddStringProperty("body", "bar")
+                                .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_build_property_existence_metadata_hits(true);
+  IcingSearchEngine icing(options, GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document0).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document1).status(), ProtoIsOk());
+
+  // Check that propertyDefined works as normal.
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::EXACT_ONLY);
+  search_spec.add_enabled_features(
+      std::string(kListFilterQueryLanguageFeature));
+  search_spec.set_query("propertyDefined(\"body\")");
+  SearchResultProto results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                                           ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document1));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(document0));
+
+  // Check that the property restriction does not apply to propertyDefined.
+  search_spec.set_query("body:propertyDefined(\"body\")");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document1));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(document0));
+
+  // Same as above.
+  search_spec.set_query("title:propertyDefined(\"body\")");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document1));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(document0));
+
+  // Same as above.
+  search_spec.set_query("non_existent_property:propertyDefined(\"body\")");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document1));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(document0));
+
+  // Property restriction works as normal for "foo" in "body".
+  search_spec.set_query("body:(foo AND propertyDefined(\"body\"))");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(1));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document0));
+
+  // Property restriction works as normal for "foo" in "non_existent_property",
+  // which should return no results.
+  search_spec.set_query(
+      "non_existent_property:(foo AND propertyDefined(\"body\"))");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), IsEmpty());
+}
+
 }  // namespace
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/icing-search-engine_suggest_test.cc b/icing/icing-search-engine_suggest_test.cc
index 1f814aa..0fa586f 100644
--- a/icing/icing-search-engine_suggest_test.cc
+++ b/icing/icing-search-engine_suggest_test.cc
@@ -144,7 +144,14 @@
                                .SetName("sender")
                                .SetDataTypeDocument(
                                    "Person", /*index_nested_properties=*/true)
-                               .SetCardinality(CARDINALITY_OPTIONAL)))
+                               .SetCardinality(CARDINALITY_OPTIONAL))
+              .AddProperty(
+                  PropertyConfigBuilder()
+                      .SetName("embed")
+                      .SetDataTypeVector(
+                          EmbeddingIndexingConfig_EmbeddingIndexingType::
+                              LINEAR_SEARCH)
+                      .SetCardinality(CARDINALITY_OPTIONAL)))
       .Build();
 }
 
@@ -464,7 +471,7 @@
   SuggestionResponse response = icing.SearchSuggestions(suggestion_spec);
   ASSERT_THAT(response.status(), ProtoIsOk());
   ASSERT_THAT(response.suggestions(),
-              UnorderedElementsAre(EqualsProto(suggestionFool)));
+              ElementsAre(EqualsProto(suggestionFool)));
 
   // namespace2 has this suggestion
   suggestion_spec.clear_namespace_filters();
@@ -472,7 +479,7 @@
   response = icing.SearchSuggestions(suggestion_spec);
   ASSERT_THAT(response.status(), ProtoIsOk());
   ASSERT_THAT(response.suggestions(),
-              UnorderedElementsAre(EqualsProto(suggestionFool)));
+              ElementsAre(EqualsProto(suggestionFool)));
 
   // delete document from namespace 1
   EXPECT_THAT(icing.Delete("namespace1", "uri1").status(), ProtoIsOk());
@@ -490,7 +497,7 @@
   response = icing.SearchSuggestions(suggestion_spec);
   ASSERT_THAT(response.status(), ProtoIsOk());
   ASSERT_THAT(response.suggestions(),
-              UnorderedElementsAre(EqualsProto(suggestionFool)));
+              ElementsAre(EqualsProto(suggestionFool)));
 }
 
 TEST_F(IcingSearchEngineSuggestTest,
@@ -536,7 +543,7 @@
   SuggestionResponse response = icing.SearchSuggestions(suggestion_spec);
   ASSERT_THAT(response.status(), ProtoIsOk());
   ASSERT_THAT(response.suggestions(),
-              UnorderedElementsAre(EqualsProto(suggestionFool)));
+              ElementsAre(EqualsProto(suggestionFool)));
 
   // Only search in namespace1,uri2
   suggestion_spec.clear_document_uri_filters();
@@ -548,7 +555,7 @@
   response = icing.SearchSuggestions(suggestion_spec);
   ASSERT_THAT(response.status(), ProtoIsOk());
   ASSERT_THAT(response.suggestions(),
-              UnorderedElementsAre(EqualsProto(suggestionFoo)));
+              ElementsAre(EqualsProto(suggestionFoo)));
 }
 
 TEST_F(IcingSearchEngineSuggestTest,
@@ -928,7 +935,7 @@
   SuggestionResponse response = icing.SearchSuggestions(suggestion_spec);
   ASSERT_THAT(response.status(), ProtoIsOk());
   ASSERT_THAT(response.suggestions(),
-              UnorderedElementsAre(EqualsProto(suggestionFool)));
+              ElementsAre(EqualsProto(suggestionFool)));
 
   // Search in subject and sender.name
   suggestion_spec.clear_type_property_filters();
@@ -1214,7 +1221,7 @@
     SuggestionResponse response = icing.SearchSuggestions(suggestion_spec);
     ASSERT_THAT(response.status(), ProtoIsOk());
     ASSERT_THAT(response.suggestions(),
-                UnorderedElementsAre(EqualsProto(suggestionFool)));
+                ElementsAre(EqualsProto(suggestionFool)));
 
     // namespace2 has this suggestion
     suggestion_spec.clear_namespace_filters();
@@ -1222,7 +1229,7 @@
     response = icing.SearchSuggestions(suggestion_spec);
     ASSERT_THAT(response.status(), ProtoIsOk());
     ASSERT_THAT(response.suggestions(),
-                UnorderedElementsAre(EqualsProto(suggestionFool)));
+                ElementsAre(EqualsProto(suggestionFool)));
   }
   // We reinitialize here so we can feed in a fake clock this time
   {
@@ -1261,7 +1268,7 @@
     response = icing.SearchSuggestions(suggestion_spec);
     ASSERT_THAT(response.status(), ProtoIsOk());
     ASSERT_THAT(response.suggestions(),
-                UnorderedElementsAre(EqualsProto(suggestionFool)));
+                ElementsAre(EqualsProto(suggestionFool)));
   }
 }
 
@@ -1334,7 +1341,7 @@
   SuggestionResponse response = icing.SearchSuggestions(suggestion_spec);
   ASSERT_THAT(response.status(), ProtoIsOk());
   ASSERT_THAT(response.suggestions(),
-              UnorderedElementsAre(EqualsProto(suggestionBarFo)));
+              ElementsAre(EqualsProto(suggestionBarFo)));
 }
 
 TEST_F(IcingSearchEngineSuggestTest, SearchSuggestionsTest_MultipleTerms_Or) {
@@ -1423,7 +1430,7 @@
   SuggestionResponse response = icing.SearchSuggestions(suggestion_spec);
   ASSERT_THAT(response.status(), ProtoIsOk());
   ASSERT_THAT(response.suggestions(),
-              UnorderedElementsAre(EqualsProto(suggestionSubjectFool)));
+              ElementsAre(EqualsProto(suggestionSubjectFool)));
 
   // Add property restriction, only search for nested sender.name
   suggestion_spec.set_prefix("sender.name:f");
@@ -1433,7 +1440,7 @@
   response = icing.SearchSuggestions(suggestion_spec);
   ASSERT_THAT(response.status(), ProtoIsOk());
   ASSERT_THAT(response.suggestions(),
-              UnorderedElementsAre(EqualsProto(suggestionSenderNameFoo)));
+              ElementsAre(EqualsProto(suggestionSenderNameFoo)));
 
   // Add property restriction, only search for nonExist section
   suggestion_spec.set_prefix("none:f");
@@ -1504,7 +1511,7 @@
   response = icing.SearchSuggestions(suggestion_spec);
   ASSERT_THAT(response.status(), ProtoIsOk());
   ASSERT_THAT(response.suggestions(),
-              UnorderedElementsAre(EqualsProto(suggestionBarCatSubjectFoo)));
+              ElementsAre(EqualsProto(suggestionBarCatSubjectFoo)));
 }
 
 TEST_F(IcingSearchEngineSuggestTest, SearchSuggestionsTest_InvalidPrefixTest) {
diff --git a/icing/icing-search-engine_task_scheduler_thread_safety_test.cc b/icing/icing-search-engine_task_scheduler_thread_safety_test.cc
new file mode 100644
index 0000000..21cd66c
--- /dev/null
+++ b/icing/icing-search-engine_task_scheduler_thread_safety_test.cc
@@ -0,0 +1,274 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <chrono>  // NOLINT
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <thread>  // NOLINT
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "icing/document-builder.h"
+#include "icing/file/filesystem.h"
+#include "icing/icing-search-engine.h"
+#include "icing/portable/equals-proto.h"
+#include "icing/portable/platform.h"
+#include "icing/proto/debug.pb.h"
+#include "icing/proto/document.pb.h"
+#include "icing/proto/document_wrapper.pb.h"
+#include "icing/proto/initialize.pb.h"
+#include "icing/proto/logging.pb.h"
+#include "icing/proto/optimize.pb.h"
+#include "icing/proto/persist.pb.h"
+#include "icing/proto/reset.pb.h"
+#include "icing/proto/schema.pb.h"
+#include "icing/proto/scoring.pb.h"
+#include "icing/proto/search.pb.h"
+#include "icing/proto/status.pb.h"
+#include "icing/proto/storage.pb.h"
+#include "icing/proto/term.pb.h"
+#include "icing/proto/usage.pb.h"
+#include "icing/schema-builder.h"
+#include "icing/testing/common-matchers.h"
+#include "icing/testing/jni-test-helpers.h"
+#include "icing/testing/test-data.h"
+#include "icing/testing/tmp-directory.h"
+#include "icing/util/clock.h"
+#include "icing/util/icu-data-file-helper.h"
+
+namespace icing {
+namespace lib {
+
+namespace {
+
+using ::testing::Eq;
+
+static constexpr int kNumIterations = 10;
+
+std::string GetTestBaseDir() { return GetTestTempDir() + "/icing"; }
+
+// Thread safety test for IcingSearchEngine's task scheduler.
+//
+// WARNING: if this test timeouts, then it's likely there is a deadlock between
+//   IcingSearchEngine and the scheduled background task. Please revisit all
+//   logics of task scheduling and task implementation to fix the issue.
+class IcingSearchEngineTaskSchedulerThreadSafetyTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
+      // If we've specified using the reverse-JNI method for segmentation (i.e.
+      // not ICU), then we won't have the ICU data file included to set up.
+      // Technically, we could choose to use reverse-JNI for segmentation AND
+      // include an ICU data file, but that seems unlikely and our current BUILD
+      // setup doesn't do this.
+      // File generated via icu_data_file rule in //icing/BUILD.
+      std::string icu_data_file_path =
+          GetTestFilePath("icing/icu.dat");
+      ICING_ASSERT_OK(
+          icu_data_file_helper::SetUpIcuDataFile(icu_data_file_path));
+    }
+    filesystem_.CreateDirectoryRecursively(GetTestBaseDir().c_str());
+  }
+
+  void TearDown() override {
+    filesystem_.DeleteDirectoryRecursively(GetTestBaseDir().c_str());
+  }
+
+  Filesystem filesystem_;
+  Clock clock_;
+};
+
+IcingSearchEngineOptions GetDefaultIcingOptions() {
+  IcingSearchEngineOptions icing_options;
+  icing_options.set_base_dir(GetTestBaseDir());
+  icing_options.set_enable_repeated_field_joins(true);
+  icing_options.set_enable_soft_index_restoration(true);
+  icing_options.set_enable_qualified_id_join_index_v3(true);
+  icing_options.set_enable_delete_propagation_from(true);
+  icing_options.set_enable_background_task_scheduler(true);
+  icing_options.set_expired_document_purge_threshold_ms(0);
+  return icing_options;
+}
+
+TEST_F(IcingSearchEngineTaskSchedulerThreadSafetyTest,
+       Destructor_TaskSchedulerShouldNotCauseDeadlockOrCrash) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  for (int i = 0; i < kNumIterations; ++i) {
+    // Initialize Icing and set schema.
+    auto icing = std::make_unique<IcingSearchEngine>(GetDefaultIcingOptions(),
+                                                     GetTestJniCache());
+    InitializeResultProto initialize_result_proto = icing->Initialize();
+
+    // There should be no recovery.
+    ASSERT_THAT(initialize_result_proto.status(), ProtoIsOk());
+    EXPECT_THAT(
+        initialize_result_proto.initialize_stats().document_store_data_status(),
+        Eq(InitializeStatsProto::NO_DATA_LOSS));
+    EXPECT_THAT(initialize_result_proto.initialize_stats()
+                    .schema_store_recovery_cause(),
+                Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result_proto.initialize_stats()
+                    .document_store_recovery_cause(),
+                Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(
+        initialize_result_proto.initialize_stats().index_restoration_cause(),
+        Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result_proto.initialize_stats()
+                    .integer_index_restoration_cause(),
+                Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result_proto.initialize_stats()
+                    .qualified_id_join_index_restoration_cause(),
+                Eq(InitializeStatsProto::NONE));
+    EXPECT_THAT(initialize_result_proto.initialize_stats()
+                    .embedding_index_restoration_cause(),
+                Eq(InitializeStatsProto::NONE));
+
+    ASSERT_THAT(icing->SetSchema(schema).status(), ProtoIsOk());
+
+    // Generate 5 person documents that expire in 2 seconds.
+    int64_t current_time_ms = clock_.GetSystemTimeMilliseconds();
+    for (int j = 0; j < 5; ++j) {
+      DocumentProto person =
+          DocumentBuilder()
+              .SetKey("namespace", "person/" + std::to_string(j))
+              .SetSchema("Person")
+              .SetCreationTimestampMs(current_time_ms)
+              .SetTtlMs(2000)
+              .AddStringProperty("name", "person name")
+              .Build();
+      ASSERT_THAT(icing->Put(person).status(), ProtoIsOk());
+    }
+
+    int64_t expected_first_expiration_time_ms = current_time_ms + 2000;
+    // Sleep until the expiration time.
+    std::this_thread::sleep_for(
+        std::chrono::milliseconds(expected_first_expiration_time_ms -
+                                  clock_.GetSystemTimeMilliseconds()));
+
+    // Destroy Icing at the same time when HandleExpiredDocuments task is about
+    // to fire. Destructor should join the task scheduler thread and destruct
+    // the task scheduler.
+    // - There should be no crash or deadlock.
+    // - PersistToDisk should be called after terminating/wating the last task
+    //   if present. The next initialization should not have data recovery.
+    icing.reset();
+  }
+}
+
+TEST_F(IcingSearchEngineTaskSchedulerThreadSafetyTest,
+       ClearAndDestroy_TaskSchedulerShouldNotCauseDeadlockOrCrash) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  for (int i = 0; i < kNumIterations; ++i) {
+    // Initialize Icing and set schema.
+    auto icing = std::make_unique<IcingSearchEngine>(GetDefaultIcingOptions(),
+                                                     GetTestJniCache());
+    ASSERT_THAT(icing->Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing->SetSchema(schema).status(), ProtoIsOk());
+
+    // Generate 5 person documents that expire in 2 seconds.
+    int64_t current_time_ms = clock_.GetSystemTimeMilliseconds();
+    for (int j = 0; j < 5; ++j) {
+      DocumentProto person =
+          DocumentBuilder()
+              .SetKey("namespace", "person/" + std::to_string(j))
+              .SetSchema("Person")
+              .SetCreationTimestampMs(current_time_ms)
+              .SetTtlMs(2000)
+              .AddStringProperty("name", "person name")
+              .Build();
+      ASSERT_THAT(icing->Put(person).status(), ProtoIsOk());
+    }
+
+    int64_t expected_first_expiration_time_ms = current_time_ms + 2000;
+    // Sleep until the expiration time.
+    std::this_thread::sleep_for(
+        std::chrono::milliseconds(expected_first_expiration_time_ms -
+                                  clock_.GetSystemTimeMilliseconds()));
+
+    // Call ClearAndDestroy() at the same time when HandleExpiredDocuments task.
+    // is about to fire.
+    // - It should either cancel or wait for the ongoing task to finish; and
+    //   destruct the task scheduler before erasing the database.
+    // - There should be no crash or deadlock.
+    ResetResultProto clear_and_destroy_result = icing->ClearAndDestroy();
+    EXPECT_THAT(clear_and_destroy_result.status(), ProtoIsOk());
+  }
+}
+
+TEST_F(IcingSearchEngineTaskSchedulerThreadSafetyTest,
+       Reset_TaskSchedulerShouldNotCauseDeadlockOrCrash) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  for (int i = 0; i < kNumIterations; ++i) {
+    // Initialize Icing and set schema.
+    auto icing = std::make_unique<IcingSearchEngine>(GetDefaultIcingOptions(),
+                                                     GetTestJniCache());
+    ASSERT_THAT(icing->Initialize().status(), ProtoIsOk());
+    ASSERT_THAT(icing->SetSchema(schema).status(), ProtoIsOk());
+
+    // Generate 5 person documents that expire in 2 seconds.
+    int64_t current_time_ms = clock_.GetSystemTimeMilliseconds();
+    for (int j = 0; j < 5; ++j) {
+      DocumentProto person =
+          DocumentBuilder()
+              .SetKey("namespace", "person/" + std::to_string(j))
+              .SetSchema("Person")
+              .SetCreationTimestampMs(current_time_ms)
+              .SetTtlMs(2000)
+              .AddStringProperty("name", "person name")
+              .Build();
+      ASSERT_THAT(icing->Put(person).status(), ProtoIsOk());
+    }
+
+    int64_t expected_first_expiration_time_ms = current_time_ms + 2000;
+    // Sleep until the expiration time.
+    std::this_thread::sleep_for(
+        std::chrono::milliseconds(expected_first_expiration_time_ms -
+                                  clock_.GetSystemTimeMilliseconds()));
+
+    // Call Reset() at the same time when HandleExpiredDocuments task is about
+    // to fire. There should be no crash or deadlock.
+    ResetResultProto reset_result = icing->Reset();
+    EXPECT_THAT(reset_result.status(), ProtoIsOk());
+  }
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/icing-search-engine_test.cc b/icing/icing-search-engine_test.cc
index 95146ed..5bc6b22 100644
--- a/icing/icing-search-engine_test.cc
+++ b/icing/icing-search-engine_test.cc
@@ -14,20 +14,21 @@
 
 #include "icing/icing-search-engine.h"
 
+#include <cstddef>
 #include <cstdint>
-#include <limits>
+#include <cstring>
 #include <memory>
 #include <string>
 #include <utility>
 
-#include "icing/text_classifier/lib3/utils/base/status.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "icing/document-builder.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/mock-filesystem.h"
+#include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/jni/jni-cache.h"
-#include "icing/portable/endian.h"
+#include "icing/legacy/index/icing-filesystem.h"
 #include "icing/portable/equals-proto.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/debug.pb.h"
@@ -64,6 +65,7 @@
 using ::testing::Gt;
 using ::testing::HasSubstr;
 using ::testing::IsEmpty;
+using ::testing::Ne;
 using ::testing::Return;
 using ::testing::SizeIs;
 using ::testing::StrEq;
@@ -237,6 +239,396 @@
               EqualsProto(expected_get_result_proto));
 }
 
+TEST_F(IcingSearchEngineTest, BatchGetDocumentResultSizeLimitOver1) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  DocumentProto document1 = CreateMessageDocument("namespace", "uri1");
+  DocumentProto biggerDocument2 =
+      CreateMessageDocument("namespace", "uri2ForBiggerDocument");
+  DocumentProto document3 = CreateMessageDocument("namespace", "uri3");
+  size_t doc1_size = document1.ByteSizeLong();
+  size_t doc3_size = document3.ByteSizeLong();
+
+  //
+  // Expected result
+  //
+  BatchGetResultProto expected_batch_get_result_proto;
+  expected_batch_get_result_proto.mutable_status()->set_code(StatusProto::OK);
+
+  // doc1 should be OK
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::OK);
+  expected_get_result_proto1.set_uri("uri1");
+  *expected_get_result_proto1.mutable_document() = document1;
+  expected_batch_get_result_proto.mutable_get_result_protos()->Add(
+      std::move(expected_get_result_proto1));
+
+  // result for doc2 should be ABORTED
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(
+      StatusProto::ABORTED);
+  expected_get_result_google::protobuf.set_uri("uri2ForBiggerDocument");
+  expected_batch_get_result_proto.mutable_get_result_protos()->Add(
+      std::move(expected_get_result_google::protobuf));
+
+  // doc3 should be ABORTED
+  GetResultProto expected_get_result_proto3;
+  expected_get_result_proto3.set_uri("uri3");
+  expected_get_result_proto3.mutable_status()->set_code(
+      StatusProto::ABORTED);
+  expected_batch_get_result_proto.mutable_get_result_protos()->Add(
+      std::move(expected_get_result_proto3));
+
+  PutDocumentRequest put_document_request;
+  put_document_request.mutable_documents()->Add(std::move(document1));
+  put_document_request.mutable_documents()->Add(std::move(biggerDocument2));
+  put_document_request.mutable_documents()->Add(std::move(document3));
+
+  ASSERT_THAT(icing.BatchPut(std::move(put_document_request)).status(),
+              ProtoIsOk());
+
+  GetResultSpecProto get_result_spec;
+  get_result_spec.set_namespace_requested("namespace");
+  get_result_spec.add_ids("uri1");
+  get_result_spec.add_ids("uri2ForBiggerDocument");
+  get_result_spec.add_ids("uri3");
+  get_result_spec.set_num_total_document_bytes_to_return(doc1_size + doc3_size);
+  ASSERT_THAT(icing.BatchGet(std::move(get_result_spec)),
+              EqualsProto(expected_batch_get_result_proto));
+}
+
+TEST_F(IcingSearchEngineTest, BatchGetDocumentResultSizeLimitOver2) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  DocumentProto document1 = CreateMessageDocument("namespace", "uri1");
+  DocumentProto biggerDocument2 =
+      CreateMessageDocument("namespace", "uri2ForBiggerDocument");
+  DocumentProto document3 = CreateMessageDocument("namespace", "uri3");
+  size_t doc1_size = document1.ByteSizeLong();
+  size_t doc2_size = biggerDocument2.ByteSizeLong();
+  size_t doc3_size = document3.ByteSizeLong();
+
+  //
+  // Expected result
+  //
+  BatchGetResultProto expected_batch_get_result_proto;
+  expected_batch_get_result_proto.mutable_status()->set_code(StatusProto::OK);
+
+  // doc1 should be OK
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::OK);
+  expected_get_result_proto1.set_uri("uri1");
+  *expected_get_result_proto1.mutable_document() = document1;
+  expected_batch_get_result_proto.mutable_get_result_protos()->Add(
+      std::move(expected_get_result_proto1));
+
+  // doc2 should be OK.
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::OK);
+  expected_get_result_google::protobuf.set_uri("uri2ForBiggerDocument");
+  *expected_get_result_google::protobuf.mutable_document() = biggerDocument2;
+  expected_batch_get_result_proto.mutable_get_result_protos()->Add(
+      std::move(expected_get_result_google::protobuf));
+
+  // doc3 should be ABORTED
+  GetResultProto expected_get_result_proto3;
+  expected_get_result_proto3.mutable_status()->set_code(
+      StatusProto::ABORTED);
+  expected_get_result_proto3.set_uri("uri3");
+  expected_batch_get_result_proto.mutable_get_result_protos()->Add(
+      std::move(expected_get_result_proto3));
+
+  PutDocumentRequest put_document_request;
+  put_document_request.mutable_documents()->Add(std::move(document1));
+  put_document_request.mutable_documents()->Add(std::move(biggerDocument2));
+  put_document_request.mutable_documents()->Add(std::move(document3));
+
+  ASSERT_THAT(icing.BatchPut(std::move(put_document_request)).status(),
+              ProtoIsOk());
+
+  GetResultSpecProto get_result_spec;
+  get_result_spec.set_namespace_requested("namespace");
+  get_result_spec.add_ids("uri1");
+  get_result_spec.add_ids("uri2ForBiggerDocument");
+  get_result_spec.add_ids("uri3");
+  get_result_spec.set_num_total_document_bytes_to_return(doc1_size + doc2_size +
+                                                         doc3_size - 1);
+  ASSERT_THAT(icing.BatchGet(std::move(get_result_spec)),
+              EqualsProto(expected_batch_get_result_proto));
+}
+
+TEST_F(IcingSearchEngineTest,
+       BatchGetDocumentResultSizeLimitOriginalErrorKept) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  DocumentProto document1 = CreateMessageDocument("namespace", "uri1");
+  DocumentProto document2 = CreateMessageDocument("namespace", "uri2");
+  DocumentProto biggerDocument3 =
+      CreateMessageDocument("namespace", "uri3ForBiggerDocument");
+  size_t doc1_size = document1.ByteSizeLong();
+
+  //
+  // Expected result
+  //
+  BatchGetResultProto expected_batch_get_result_proto;
+  expected_batch_get_result_proto.mutable_status()->set_code(StatusProto::OK);
+
+  // doc1 should be OK
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::OK);
+  expected_get_result_proto1.set_uri("uri1");
+  *expected_get_result_proto1.mutable_document() = document1;
+  expected_batch_get_result_proto.mutable_get_result_protos()->Add(
+      std::move(expected_get_result_proto1));
+
+  // uriNotExist should not be found.
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.set_uri("uriNotExist");
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_google::protobuf.mutable_status()->set_message(
+      "Document (namespace, uriNotExist) not found.");
+  expected_batch_get_result_proto.mutable_get_result_protos()->Add(
+      std::move(expected_get_result_google::protobuf));
+
+  // doc3 should be ABORTED
+  GetResultProto expected_get_result_proto3;
+  expected_get_result_proto3.mutable_status()->set_code(
+      StatusProto::ABORTED);
+  expected_get_result_proto3.set_uri("uri3ForBiggerDocument");
+  expected_batch_get_result_proto.mutable_get_result_protos()->Add(
+      std::move(expected_get_result_proto3));
+
+  // uriNotExist should be ABORTED as we have reached the limit.
+  // Even though the doc doesn't exist.
+  GetResultProto expected_get_result_proto4;
+  expected_get_result_proto4.mutable_status()->set_code(
+      StatusProto::ABORTED);
+  expected_get_result_proto4.set_uri("uriNotExist");
+  expected_batch_get_result_proto.mutable_get_result_protos()->Add(
+      std::move(expected_get_result_proto4));
+
+  PutDocumentRequest put_document_request;
+  put_document_request.mutable_documents()->Add(std::move(document1));
+  put_document_request.mutable_documents()->Add(std::move(document2));
+  put_document_request.mutable_documents()->Add(std::move(biggerDocument3));
+
+  ASSERT_THAT(icing.BatchPut(std::move(put_document_request)).status(),
+              ProtoIsOk());
+
+  GetResultSpecProto get_result_spec;
+  get_result_spec.set_namespace_requested("namespace");
+  get_result_spec.add_ids("uri1");
+  get_result_spec.add_ids("uriNotExist");
+  get_result_spec.add_ids("uri3ForBiggerDocument");
+  get_result_spec.add_ids("uriNotExist");
+  get_result_spec.set_num_total_document_bytes_to_return(doc1_size);
+  ASSERT_THAT(icing.BatchGet(std::move(get_result_spec)),
+              EqualsProto(expected_batch_get_result_proto));
+}
+
+TEST_F(IcingSearchEngineTest, BatchGetDocumentAlwaysReturnOneDoc) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  DocumentProto document1 = CreateMessageDocument("namespace", "uri1");
+  DocumentProto document2 = CreateMessageDocument("namespace", "uri2");
+  DocumentProto document3 = CreateMessageDocument("namespace", "uri3");
+
+  //
+  // Expected result
+  //
+  BatchGetResultProto expected_batch_get_result_proto;
+  expected_batch_get_result_proto.mutable_status()->set_code(StatusProto::OK);
+
+  // doc1 should be OK
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::OK);
+  expected_get_result_proto1.set_uri("uri1");
+  *expected_get_result_proto1.mutable_document() = document1;
+  expected_batch_get_result_proto.mutable_get_result_protos()->Add(
+      std::move(expected_get_result_proto1));
+
+  // result for doc2 should be ABORTED
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::ABORTED);
+  expected_get_result_google::protobuf.set_uri("uri2");
+  expected_batch_get_result_proto.mutable_get_result_protos()->Add(
+      std::move(expected_get_result_google::protobuf));
+
+  // doc3 should be ABORTED
+  GetResultProto expected_get_result_proto3;
+  expected_get_result_proto3.set_uri("uri3");
+  expected_get_result_proto3.mutable_status()->set_code(StatusProto::ABORTED);
+  expected_batch_get_result_proto.mutable_get_result_protos()->Add(
+      std::move(expected_get_result_proto3));
+
+  PutDocumentRequest put_document_request;
+  put_document_request.mutable_documents()->Add(std::move(document1));
+  put_document_request.mutable_documents()->Add(std::move(document2));
+  put_document_request.mutable_documents()->Add(std::move(document3));
+
+  ASSERT_THAT(icing.BatchPut(std::move(put_document_request)).status(),
+              ProtoIsOk());
+
+  GetResultSpecProto get_result_spec;
+  get_result_spec.set_namespace_requested("namespace");
+  get_result_spec.add_ids("uri1");
+  get_result_spec.add_ids("uri2");
+  get_result_spec.add_ids("uri3");
+  // very small limit. We should always return at least one doc.
+  get_result_spec.set_num_total_document_bytes_to_return(1);
+  ASSERT_THAT(icing.BatchGet(std::move(get_result_spec)),
+              EqualsProto(expected_batch_get_result_proto));
+}
+
+TEST_F(IcingSearchEngineTest, BatchGetDocumentResultSizeLimitNotOver) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  DocumentProto document1 = CreateMessageDocument("namespace", "uri1");
+  DocumentProto biggerDocument2 =
+      CreateMessageDocument("namespace", "uri2ForBiggerDocument");
+  DocumentProto document3 = CreateMessageDocument("namespace", "uri3");
+  size_t doc1_size = document1.ByteSizeLong();
+  size_t doc2_size = biggerDocument2.ByteSizeLong();
+  size_t doc3_size = document3.ByteSizeLong();
+
+  //
+  // Expected result
+  //
+  BatchGetResultProto expected_batch_get_result_proto;
+  expected_batch_get_result_proto.mutable_status()->set_code(StatusProto::OK);
+
+  // doc1 should be OK
+  GetResultProto expected_get_result_proto1;
+  expected_get_result_proto1.mutable_status()->set_code(StatusProto::OK);
+  expected_get_result_proto1.set_uri("uri1");
+  *expected_get_result_proto1.mutable_document() = document1;
+  expected_batch_get_result_proto.mutable_get_result_protos()->Add(
+      std::move(expected_get_result_proto1));
+
+  // doc2 should be OK.
+  GetResultProto expected_get_result_google::protobuf;
+  expected_get_result_google::protobuf.mutable_status()->set_code(StatusProto::OK);
+  expected_get_result_google::protobuf.set_uri("uri2ForBiggerDocument");
+  *expected_get_result_google::protobuf.mutable_document() = biggerDocument2;
+  expected_batch_get_result_proto.mutable_get_result_protos()->Add(
+      std::move(expected_get_result_google::protobuf));
+
+  // doc3 should be OK
+  GetResultProto expected_get_result_proto3;
+  expected_get_result_proto3.mutable_status()->set_code(StatusProto::OK);
+  expected_get_result_proto3.set_uri("uri3");
+  *expected_get_result_proto3.mutable_document() = document3;
+  expected_batch_get_result_proto.mutable_get_result_protos()->Add(
+      std::move(expected_get_result_proto3));
+
+  PutDocumentRequest put_document_request;
+  put_document_request.mutable_documents()->Add(std::move(document1));
+  put_document_request.mutable_documents()->Add(std::move(biggerDocument2));
+  put_document_request.mutable_documents()->Add(std::move(document3));
+
+  ASSERT_THAT(icing.BatchPut(std::move(put_document_request)).status(),
+              ProtoIsOk());
+
+  GetResultSpecProto get_result_spec1;
+  get_result_spec1.set_namespace_requested("namespace");
+  get_result_spec1.add_ids("uri1");
+  get_result_spec1.add_ids("uri2ForBiggerDocument");
+  get_result_spec1.add_ids("uri3");
+  get_result_spec1.set_num_total_document_bytes_to_return(
+      doc1_size + doc2_size + doc3_size);
+  ASSERT_THAT(icing.BatchGet(std::move(get_result_spec1)),
+              EqualsProto(expected_batch_get_result_proto));
+
+  // Return bytes limit with default value(INT_MAX).
+  GetResultSpecProto get_result_spec2;
+  get_result_spec2.set_namespace_requested("namespace");
+  get_result_spec2.add_ids("uri1");
+  get_result_spec2.add_ids("uri2ForBiggerDocument");
+  get_result_spec2.add_ids("uri3");
+
+  ASSERT_THAT(icing.BatchGet(std::move(get_result_spec2)),
+              EqualsProto(expected_batch_get_result_proto));
+}
+
+TEST_F(IcingSearchEngineTest, BatchGetDocumentResultSizeLimitInvalidValue) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  GetResultSpecProto get_result_spec;
+  get_result_spec.set_namespace_requested("namespace");
+
+  BatchGetResultProto expected_batch_get_result_proto;
+  expected_batch_get_result_proto.mutable_status()->set_code(
+      StatusProto::INVALID_ARGUMENT);
+  expected_batch_get_result_proto.mutable_status()->set_message(
+      "num_total_document_bytes_to_return must be greater than 0.");
+
+  get_result_spec.set_num_total_document_bytes_to_return(0);
+  ASSERT_THAT(icing.BatchGet(GetResultSpecProto(get_result_spec)),
+              EqualsProto(expected_batch_get_result_proto));
+
+  get_result_spec.set_num_total_document_bytes_to_return(-1);
+  ASSERT_THAT(icing.BatchGet(std::move(get_result_spec)),
+              EqualsProto(expected_batch_get_result_proto));
+}
+
+TEST_F(IcingSearchEngineTest, BatchGetNotInitialized) {
+  // Declare but don't initialize icing
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  GetResultSpecProto get_result_spec;
+  get_result_spec.set_namespace_requested("namespace");
+  get_result_spec.add_ids("uri1");
+  get_result_spec.add_ids("uri2");
+  get_result_spec.set_num_total_document_bytes_to_return(1000);
+
+  BatchGetResultProto result = icing.BatchGet(std::move(get_result_spec));
+  EXPECT_THAT(result.status(), ProtoStatusIs(StatusProto::FAILED_PRECONDITION));
+  EXPECT_THAT(result.get_result_protos(), SizeIs(2));
+  for (const auto& get_result : result.get_result_protos()) {
+    EXPECT_THAT(get_result.status(),
+                ProtoStatusIs(StatusProto::FAILED_PRECONDITION));
+  }
+}
+
+TEST_F(IcingSearchEngineTest, BatchPutNotInitialized) {
+  // Declare but don't initialize icing
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  PutDocumentRequest put_document_request;
+  *put_document_request.mutable_documents()->Add() =
+      DocumentBuilder()
+          .SetKey("namespace", "uri1")
+          .SetSchema("Email")
+          .SetCreationTimestampMs(1000)
+          .AddStringProperty("subject", "subject1")
+          .Build();
+  *put_document_request.mutable_documents()->Add() =
+      DocumentBuilder()
+          .SetKey("namespace", "uri2")
+          .SetSchema("Email")
+          .SetCreationTimestampMs(1000)
+          .AddStringProperty("subject", "subject2")
+          .Build();
+
+  BatchPutResultProto result = icing.BatchPut(std::move(put_document_request));
+  EXPECT_THAT(result.status(), ProtoStatusIs(StatusProto::FAILED_PRECONDITION));
+  EXPECT_THAT(result.put_result_protos(), SizeIs(2));
+  for (const auto& put_result : result.put_result_protos()) {
+    EXPECT_THAT(put_result.status(),
+                ProtoStatusIs(StatusProto::FAILED_PRECONDITION));
+  }
+}
+
 TEST_F(IcingSearchEngineTest, GetDocumentWithBadString) {
   IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
@@ -1062,8 +1454,53 @@
                                   expected_search_result_proto));
 }
 
-TEST_F(IcingSearchEngineTest, NoPersistToDiskLosesAllDocumentsAndIndex) {
-  IcingSearchEngine icing1(GetDefaultIcingOptions(), GetTestJniCache());
+TEST_F(IcingSearchEngineTest, PersistToDiskLogging) {
+  DocumentProto document = CreateMessageDocument("namespace", "uri");
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetTimerElapsedMilliseconds(10);
+  // Add schema and documents to our first icing1 instance.
+
+  IcingSearchEngineOptions icing_options;
+  icing_options.set_base_dir(GetTestBaseDir());
+  icing_options.set_enable_blob_store(true);
+  icing_options.set_enable_marker_file_for_optimize(true);
+
+  TestIcingSearchEngine icing(icing_options, std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document).status(), ProtoIsOk());
+
+  PersistToDiskResultProto persist_result =
+      icing.PersistToDisk(PersistType::FULL);
+  EXPECT_THAT(persist_result.status(), ProtoIsOk());
+  PersistToDiskStatsProto persist_stats = persist_result.persist_stats();
+  EXPECT_THAT(persist_stats.latency_ms(), Eq(10));
+  EXPECT_THAT(persist_stats.persist_type(), Eq(PersistType::FULL));
+  EXPECT_THAT(persist_stats.blob_store_persist_latency_ms(), Eq(10));
+  EXPECT_THAT(persist_stats.document_store_total_persist_latency_ms(), Eq(10));
+  EXPECT_THAT(persist_stats.document_store_components_persist_latency_ms(),
+              Eq(10));
+  EXPECT_THAT(persist_stats.document_store_checksum_update_latency_ms(),
+              Eq(10));
+  EXPECT_THAT(persist_stats.document_log_checksum_update_latency_ms(), Eq(10));
+  EXPECT_THAT(persist_stats.document_log_data_sync_latency_ms(), Eq(10));
+  EXPECT_THAT(persist_stats.schema_store_persist_latency_ms(), Eq(10));
+  EXPECT_THAT(persist_stats.index_persist_latency_ms(), Eq(10));
+  EXPECT_THAT(persist_stats.integer_index_persist_latency_ms(), Eq(10));
+  EXPECT_THAT(persist_stats.qualified_id_join_index_persist_latency_ms(),
+              Eq(10));
+  EXPECT_THAT(persist_stats.embedding_index_persist_latency_ms(), Eq(10));
+}
+
+TEST_F(
+    IcingSearchEngineTest,
+    NoPersistToDisk_protoLogNewHeaderFormatFlagOffLosesAllDocumentsAndIndex) {
+  IcingSearchEngineOptions icing_options = GetDefaultIcingOptions();
+  icing_options.set_enable_proto_log_new_header_format(false);
+  IcingSearchEngine icing1(icing_options, GetTestJniCache());
+
   EXPECT_THAT(icing1.Initialize().status(), ProtoIsOk());
   EXPECT_THAT(icing1.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
   DocumentProto document = CreateMessageDocument("namespace", "uri");
@@ -1111,6 +1548,261 @@
                                   expected_search_result_proto));
 }
 
+TEST_F(
+    IcingSearchEngineTest,
+    NoPersistToDisk_protoLogNewHeaderFormatFlagOnWithImplicitFlushKeepsDocumentsAndRebuildIndex) {
+  IcingSearchEngineOptions icing_options = GetDefaultIcingOptions();
+  icing_options.set_enable_proto_log_new_header_format(true);
+  IcingSearchEngine icing1(icing_options, GetTestJniCache());
+
+  EXPECT_THAT(icing1.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing1.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+  DocumentProto document = CreateMessageDocument("namespace", "uri");
+  EXPECT_THAT(icing1.Put(document).status(), ProtoIsOk());
+  EXPECT_THAT(
+      icing1.Get("namespace", "uri", GetResultSpecProto::default_instance())
+          .document(),
+      EqualsProto(document));
+
+  // It's intentional that no PersistToDisk call is made before initializing a
+  // second instance of icing. We won't lose ground truth, but we will get IO
+  // errors (due to mismatch checksums) for derived files and rebuild them.
+
+  IcingSearchEngine icing2(icing_options, GetTestJniCache());
+  InitializeResultProto init_result = icing2.Initialize();
+  EXPECT_THAT(init_result.status(), ProtoIsOk());
+  EXPECT_THAT(init_result.initialize_stats().schema_store_recovery_cause(),
+              Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(init_result.initialize_stats().document_store_data_status(),
+              Eq(InitializeStatsProto::NO_DATA_LOSS));
+  EXPECT_THAT(init_result.initialize_stats().document_store_recovery_cause(),
+              Eq(InitializeStatsProto::IO_ERROR));
+  EXPECT_THAT(init_result.initialize_stats().index_restoration_cause(),
+              Eq(InitializeStatsProto::IO_ERROR));
+
+  // The document can still be retrieved.
+  EXPECT_THAT(
+      icing2.Get("namespace", "uri", GetResultSpecProto::default_instance())
+          .document(),
+      EqualsProto(document));
+
+  // Searching also should get the document since index is rebuilt.
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::PREFIX);
+  search_spec.set_query("message");  // Content in the Message document.
+
+  SearchResultProto expected_search_result_proto;
+  expected_search_result_proto.mutable_status()->set_code(StatusProto::OK);
+  *expected_search_result_proto.mutable_results()->Add()->mutable_document() =
+      document;
+
+  SearchResultProto actual_results =
+      icing2.Search(search_spec, GetDefaultScoringSpec(),
+                    ResultSpecProto::default_instance());
+  EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
+                                  expected_search_result_proto));
+}
+
+TEST_F(
+    IcingSearchEngineTest,
+    NoPersistToDisk_protoLogNewHeaderFormatFlagOnWithoutImplicitFlushLosesNewDocumentsAndIndex) {
+  // Mock the filesystem for document_log to avoid implicit flush when writing,
+  // and only DataSync will do the actual write and flush.
+  std::string mock_file_buffer;
+  off_t document_log_fd_offset = -1;
+  int document_log_fd = -1;
+
+  auto mock_filesystem = std::make_unique<MockFilesystem>();
+  MockFilesystem* mock_filesystem_ptr = mock_filesystem.get();
+  ON_CALL(*mock_filesystem, OpenForAppend(HasSubstr("document_log")))
+      .WillByDefault([mock_filesystem_ptr, &mock_file_buffer, &document_log_fd,
+                      &document_log_fd_offset](const char* file_name) -> int {
+        // Call the real OpenForAppend() get the file descriptor, and capture it
+        // in the test fixture.
+        document_log_fd =
+            mock_filesystem_ptr->Filesystem::OpenForAppend(file_name);
+        int64_t file_size =
+            mock_filesystem_ptr->Filesystem::GetFileSize(document_log_fd);
+        document_log_fd_offset = file_size;
+
+        // Load data from disk to the mock file buffer.
+        mock_file_buffer.resize(file_size);
+        mock_filesystem_ptr->Filesystem::PRead(
+            document_log_fd, mock_file_buffer.data(), file_size,
+            /*offset=*/0);
+        return document_log_fd;
+      });
+  ON_CALL(*mock_filesystem, GetCurrentPosition(_))
+      .WillByDefault([mock_filesystem_ptr, &document_log_fd,
+                      &document_log_fd_offset](int fd) -> int64_t {
+        if (fd != document_log_fd) {
+          return mock_filesystem_ptr->Filesystem::GetCurrentPosition(fd);
+        }
+
+        return document_log_fd_offset;
+      });
+  ON_CALL(*mock_filesystem, Read(A<int>(), _, _))
+      .WillByDefault([mock_filesystem_ptr, &mock_file_buffer, &document_log_fd,
+                      &document_log_fd_offset](int fd, void* buf,
+                                               size_t buf_size) -> ssize_t {
+        if (fd != document_log_fd) {
+          return mock_filesystem_ptr->Filesystem::Read(fd, buf, buf_size);
+        }
+
+        memcpy(buf, mock_file_buffer.data() + document_log_fd_offset, buf_size);
+        document_log_fd_offset += buf_size;
+        return buf_size;
+      });
+  ON_CALL(*mock_filesystem, Write(A<int>(), _, _))
+      .WillByDefault([mock_filesystem_ptr, &mock_file_buffer, &document_log_fd,
+                      &document_log_fd_offset](int fd, const void* data,
+                                               size_t size) -> bool {
+        if (fd != document_log_fd) {
+          return mock_filesystem_ptr->Filesystem::Write(fd, data, size);
+        }
+
+        // Extend the buffer if needed.
+        if (document_log_fd_offset + size > mock_file_buffer.size()) {
+          mock_file_buffer.resize(document_log_fd_offset + size);
+        }
+
+        // Write to the mock file buffer and adjust the fd offset.
+        memcpy(mock_file_buffer.data() + document_log_fd_offset, data, size);
+        document_log_fd_offset += size;
+        return true;
+      });
+  ON_CALL(*mock_filesystem, PRead(A<int>(), _, _, _))
+      .WillByDefault(
+          [mock_filesystem_ptr, &mock_file_buffer, &document_log_fd](
+              int fd, void* buf, size_t buf_size, off_t offset) -> ssize_t {
+            if (fd != document_log_fd) {
+              return mock_filesystem_ptr->Filesystem::PRead(fd, buf, buf_size,
+                                                            offset);
+            }
+
+            memcpy(buf, mock_file_buffer.data() + offset, buf_size);
+            return buf_size;
+          });
+  ON_CALL(*mock_filesystem, PWrite(A<int>(), _, _, _))
+      .WillByDefault([mock_filesystem_ptr, &mock_file_buffer, &document_log_fd](
+                         int fd, off_t offset, const void* data,
+                         size_t data_size) -> bool {
+        if (fd != document_log_fd) {
+          return mock_filesystem_ptr->Filesystem::PWrite(fd, offset, data,
+                                                         data_size);
+        }
+
+        // Extend the buffer if needed.
+        if (offset + data_size > mock_file_buffer.size()) {
+          mock_file_buffer.resize(offset + data_size);
+        }
+        // Write to the mock file buffer.
+        memcpy(mock_file_buffer.data() + offset, data, data_size);
+        return true;
+      });
+  ON_CALL(*mock_filesystem, DataSync(_))
+      .WillByDefault([mock_filesystem_ptr, &mock_file_buffer,
+                      &document_log_fd](int fd) -> bool {
+        if (fd != document_log_fd) {
+          return mock_filesystem_ptr->Filesystem::DataSync(fd);
+        }
+
+        mock_filesystem_ptr->Filesystem::PWrite(
+            fd, /*offset=*/0, mock_file_buffer.data(), mock_file_buffer.size());
+        return mock_filesystem_ptr->Filesystem::DataSync(fd);
+      });
+
+  IcingSearchEngineOptions icing_options = GetDefaultIcingOptions();
+  icing_options.set_enable_proto_log_new_header_format(true);
+  TestIcingSearchEngine icing1(icing_options, std::move(mock_filesystem),
+                               std::make_unique<IcingFilesystem>(),
+                               std::make_unique<FakeClock>(),
+                               GetTestJniCache());
+
+  EXPECT_THAT(icing1.Initialize().status(), ProtoIsOk());
+  // After initialization, document log fd should be captured.
+  ASSERT_THAT(document_log_fd, Ne(-1));
+
+  EXPECT_THAT(icing1.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  DocumentProto document1 = CreateMessageDocument("namespace", "uri1");
+  DocumentProto document2 = CreateMessageDocument("namespace", "uri2");
+  DocumentProto document3 = CreateMessageDocument("namespace", "uri3");
+
+  // Put document1 and PersistToDisk to flush it to disk.
+  EXPECT_THAT(icing1.Put(document1).status(), ProtoIsOk());
+  EXPECT_THAT(icing1.PersistToDisk(PersistType::LITE).status(), ProtoIsOk());
+
+  // Put document2 and document3 without flushing.
+  EXPECT_THAT(icing1.Put(document2).status(), ProtoIsOk());
+  EXPECT_THAT(icing1.Put(document3).status(), ProtoIsOk());
+
+  // Sanity check that document1, document2 and document3 are present.
+  EXPECT_THAT(
+      icing1.Get("namespace", "uri1", GetResultSpecProto::default_instance())
+          .document(),
+      EqualsProto(document1));
+  EXPECT_THAT(
+      icing1.Get("namespace", "uri2", GetResultSpecProto::default_instance())
+          .document(),
+      EqualsProto(document2));
+  EXPECT_THAT(
+      icing1.Get("namespace", "uri3", GetResultSpecProto::default_instance())
+          .document(),
+      EqualsProto(document3));
+
+  // When writing a document, we separate it into several steps:
+  // - Write document content to document_log (unsynced tail).
+  // - Compute unsynced tail checksum and header checksum.
+  // - PWrite the header.
+  //
+  // Let's simulate the following scenario by only flushing contents to disk:
+  // - document2 and document3 contents are flushed into disk.
+  // - But before the checksums are computed, there is a power loss and the new
+  //   checksums are never written.
+
+  int64_t content_offset =
+      PortableFileBackedProtoLog<DocumentProto>::kHeaderReservedBytes;
+  ASSERT_TRUE(filesystem()->PWrite(document_log_fd, content_offset,
+                                   mock_file_buffer.data() + content_offset,
+                                   mock_file_buffer.size() - content_offset));
+  ASSERT_TRUE(filesystem()->DataSync(document_log_fd));
+
+  // Create a second instance of icing.
+  // - Document2 and document3 contents are in the file.
+  // - But unsynced tail checksum doesn't match.
+  //
+  // Therefore, document log will rewound to document1 and discard document2 +
+  // document3.
+  IcingSearchEngine icing2(icing_options, GetTestJniCache());
+  InitializeResultProto init_result = icing2.Initialize();
+  EXPECT_THAT(init_result.status(), ProtoIsOk());
+  EXPECT_THAT(init_result.initialize_stats().schema_store_recovery_cause(),
+              Eq(InitializeStatsProto::NONE));
+  EXPECT_THAT(init_result.initialize_stats().document_store_data_status(),
+              Eq(InitializeStatsProto::PARTIAL_LOSS));
+  EXPECT_THAT(init_result.initialize_stats().document_store_recovery_cause(),
+              Eq(InitializeStatsProto::DATA_LOSS));
+  EXPECT_THAT(init_result.initialize_stats().index_restoration_cause(),
+              Eq(InitializeStatsProto::IO_ERROR));
+
+  // document1 can still be retrieved.
+  EXPECT_THAT(
+      icing2.Get("namespace", "uri1", GetResultSpecProto::default_instance())
+          .document(),
+      EqualsProto(document1));
+
+  // document2 and document3 are lost.
+  EXPECT_THAT(
+      icing2.Get("namespace", "uri2", GetResultSpecProto::default_instance())
+          .status(),
+      ProtoStatusIs(StatusProto::NOT_FOUND));
+  EXPECT_THAT(
+      icing2.Get("namespace", "uri3", GetResultSpecProto::default_instance())
+          .status(),
+      ProtoStatusIs(StatusProto::NOT_FOUND));
+}
+
 TEST_F(IcingSearchEngineTest, PersistToDiskLiteSavesGroundTruth) {
   DocumentProto document = CreateMessageDocument("namespace", "uri");
 
@@ -1494,6 +2186,49 @@
   ASSERT_THAT(result.status(), ProtoIsOk());
 }
 
+TEST_F(IcingSearchEngineTest, ClearAndDestroySucceeds) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+
+  // Obtain empty size of the base directory.
+  int64_t empty_state_size =
+      filesystem()->GetFileDiskUsage(GetTestBaseDir().c_str());
+
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  // Simple put and get
+  ASSERT_THAT(icing.Put(CreateMessageDocument("namespace", "uri")).status(),
+              ProtoIsOk());
+
+  GetResultProto expected_get_result_proto;
+  expected_get_result_proto.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto.mutable_document() =
+      CreateMessageDocument("namespace", "uri");
+  ASSERT_THAT(
+      icing.Get("namespace", "uri", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto));
+
+  // Sanity check that things have been added
+  EXPECT_THAT(filesystem()->GetDiskUsage(GetTestBaseDir().c_str()),
+              Gt(empty_state_size));
+  EXPECT_EQ(filesystem()->DirectoryExists(GetTestBaseDir().c_str()), true);
+
+  // Clear all data for the icing instance
+  ResetResultProto clear_result = icing.ClearAndDestroy();
+  EXPECT_THAT(clear_result.status(), ProtoIsOk());
+
+  // Check that the directory no longer exists after deletion.
+  EXPECT_EQ(filesystem()->DirectoryExists(GetTestBaseDir().c_str()), false);
+
+  // Try to put and get again, but it should fail since the instance is
+  // uninitialized.
+  EXPECT_THAT(icing.Put(CreateMessageDocument("namespace", "uri")).status(),
+              ProtoStatusIs(StatusProto::FAILED_PRECONDITION));
+  EXPECT_THAT(
+      icing.Get("namespace", "uri", GetResultSpecProto::default_instance())
+          .status(),
+      ProtoStatusIs(StatusProto::FAILED_PRECONDITION));
+}
 }  // namespace
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/index/embed/doc-hit-info-iterator-embedding.cc b/icing/index/embed/doc-hit-info-iterator-embedding-v1.cc
similarity index 63%
rename from icing/index/embed/doc-hit-info-iterator-embedding.cc
rename to icing/index/embed/doc-hit-info-iterator-embedding-v1.cc
index 292fb03..b991e4a 100644
--- a/icing/index/embed/doc-hit-info-iterator-embedding.cc
+++ b/icing/index/embed/doc-hit-info-iterator-embedding-v1.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "icing/index/embed/doc-hit-info-iterator-embedding.h"
+#include "icing/index/embed/doc-hit-info-iterator-embedding-v1.h"
 
 #include <cstdint>
 #include <memory>
@@ -26,7 +26,6 @@
 #include "icing/index/embed/embedding-index.h"
 #include "icing/index/embed/embedding-query-results.h"
 #include "icing/index/embed/embedding-scorer.h"
-#include "icing/index/embed/posting-list-embedding-hit-accessor.h"
 #include "icing/index/hit/doc-hit-info.h"
 #include "icing/index/hit/hit.h"
 #include "icing/proto/search.pb.h"
@@ -40,50 +39,55 @@
 namespace icing {
 namespace lib {
 
-libtextclassifier3::StatusOr<std::unique_ptr<DocHitInfoIteratorEmbedding>>
-DocHitInfoIteratorEmbedding::Create(
+libtextclassifier3::StatusOr<std::unique_ptr<DocHitInfoIteratorEmbeddingV1>>
+DocHitInfoIteratorEmbeddingV1::Create(
     const PropertyProto::VectorProto* query,
     SearchSpecProto::EmbeddingQueryMetricType::Code metric_type,
-    double score_low, double score_high, bool get_embedding_match_info,
+    double score_low, double score_high,
     EmbeddingQueryResults::EmbeddingQueryMatchInfoMap* info_map,
+    std::vector<double>* global_scores,
+    std::vector<EmbeddingMatchInfos::EmbeddingMatchSectionInfo>*
+        global_section_infos,
     const EmbeddingIndex* embedding_index, const DocumentStore* document_store,
     const SchemaStore* schema_store, int64_t current_time_ms) {
   ICING_RETURN_ERROR_IF_NULL(query);
   ICING_RETURN_ERROR_IF_NULL(embedding_index);
   ICING_RETURN_ERROR_IF_NULL(info_map);
+  ICING_RETURN_ERROR_IF_NULL(global_scores);
   ICING_RETURN_ERROR_IF_NULL(document_store);
   ICING_RETURN_ERROR_IF_NULL(schema_store);
 
-  libtextclassifier3::StatusOr<std::unique_ptr<PostingListEmbeddingHitAccessor>>
-      pl_accessor_or = embedding_index->GetAccessorForVector(*query);
-  std::unique_ptr<PostingListEmbeddingHitAccessor> pl_accessor;
-  if (pl_accessor_or.ok()) {
-    pl_accessor = std::move(pl_accessor_or).ValueOrDie();
-  } else if (absl_ports::IsNotFound(pl_accessor_or.status())) {
+  libtextclassifier3::StatusOr<
+      std::unique_ptr<EmbeddingIndex::EmbeddingHitAccessor>>
+      embedding_hit_accessor_or = embedding_index->GetAccessorForVector(*query);
+  std::unique_ptr<EmbeddingIndex::EmbeddingHitAccessor> embedding_hit_accessor;
+  if (embedding_hit_accessor_or.ok()) {
+    embedding_hit_accessor = std::move(embedding_hit_accessor_or).ValueOrDie();
+  } else if (absl_ports::IsNotFound(embedding_hit_accessor_or.status())) {
     // A not-found error should be fine, since that means there is no matching
     // embedding hits in the index.
-    pl_accessor = nullptr;
+    embedding_hit_accessor = nullptr;
   } else {
     // Otherwise, return the error as is.
-    return pl_accessor_or.status();
+    return embedding_hit_accessor_or.status();
   }
 
   ICING_ASSIGN_OR_RETURN(std::unique_ptr<EmbeddingScorer> embedding_scorer,
                          EmbeddingScorer::Create(metric_type));
 
-  return std::unique_ptr<DocHitInfoIteratorEmbedding>(
-      new DocHitInfoIteratorEmbedding(
+  return std::unique_ptr<DocHitInfoIteratorEmbeddingV1>(
+      new DocHitInfoIteratorEmbeddingV1(
           query, metric_type, std::move(embedding_scorer), score_low,
-          score_high, get_embedding_match_info, info_map, embedding_index,
-          std::move(pl_accessor), document_store, schema_store,
-          current_time_ms));
+          score_high, info_map, global_scores, global_section_infos,
+          embedding_index, std::move(embedding_hit_accessor), document_store,
+          schema_store, current_time_ms));
 }
 
 libtextclassifier3::StatusOr<const EmbeddingHit*>
-DocHitInfoIteratorEmbedding::AdvanceToNextEmbeddingHit() {
+DocHitInfoIteratorEmbeddingV1::AdvanceToNextEmbeddingHit() {
   if (cached_embedding_hits_idx_ == cached_embedding_hits_.size()) {
     ICING_ASSIGN_OR_RETURN(cached_embedding_hits_,
-                           posting_list_accessor_->GetNextHitsBatch());
+                           embedding_hit_accessor_->GetNextHitsBatch());
     cached_embedding_hits_idx_ = 0;
     if (cached_embedding_hits_.empty()) {
       no_more_hit_ = true;
@@ -94,14 +98,25 @@
       cached_embedding_hits_[cached_embedding_hits_idx_];
   if (doc_hit_info_.document_id() == kInvalidDocumentId) {
     doc_hit_info_.set_document_id(embedding_hit.basic_hit().document_id());
-    current_allowed_sections_mask_ =
-        ComputeAllowedSectionsMask(doc_hit_info_.document_id());
+    if (DoesDocumentPassAllFilters(doc_hit_info_.document_id())) {
+      current_allowed_sections_mask_ =
+          ComputeAllowedSectionsMask(doc_hit_info_.document_id());
 
-    schema_type_id_ = document_store_.GetSchemaTypeId(
-        doc_hit_info_.document_id(), current_time_ms_);
-    if (schema_type_id_ == kInvalidSchemaTypeId) {
-      // This means that the document is deleted or expired, so update
-      // current_allowed_sections_mask_ to skip the document.
+      schema_type_id_ = document_store_.GetSchemaTypeId(
+          doc_hit_info_.document_id(), current_time_ms_);
+      if (schema_type_id_ == kInvalidSchemaTypeId) {
+        // This means that the document is deleted or expired, so update
+        // current_allowed_sections_mask_ to skip the document.
+        current_allowed_sections_mask_ = kSectionIdMaskNone;
+      } else {
+        ICING_ASSIGN_OR_RETURN(
+            schema_name_hash_,
+            schema_store_.GetSchemaNameHash(schema_type_id_));
+      }
+    } else {
+      // This means that the document is filtered out by the document filter
+      // predicate, so update current_allowed_sections_mask_ to skip the
+      // document.
       current_allowed_sections_mask_ = kSectionIdMaskNone;
     }
   } else if (doc_hit_info_.document_id() !=
@@ -113,14 +128,15 @@
 }
 
 libtextclassifier3::Status
-DocHitInfoIteratorEmbedding::AdvanceToNextUnfilteredDocument() {
-  if (no_more_hit_ || posting_list_accessor_ == nullptr) {
+DocHitInfoIteratorEmbeddingV1::AdvanceToNextUnfilteredDocument() {
+  if (no_more_hit_ || embedding_hit_accessor_ == nullptr) {
     return absl_ports::ResourceExhaustedError(
         "No more DocHitInfos in iterator");
   }
 
   doc_hit_info_ = DocHitInfo(kInvalidDocumentId, kSectionIdMaskNone);
   schema_type_id_ = kInvalidSchemaTypeId;
+  schema_name_hash_ = 0;
   EmbeddingMatchInfos* matched_infos = nullptr;
   current_allowed_sections_mask_ = kSectionIdMaskAll;
   SectionId current_section_id = kInvalidSectionId;
@@ -142,6 +158,12 @@
       continue;
     }
 
+    if (schema_type_id_ == kInvalidSchemaTypeId) {
+      // This should never happen, because current_allowed_sections_mask_ would
+      // have been updated to kSectionIdMaskNone for this case.
+      continue;
+    }
+
     // We've reached a new section. Reset the match count and retrieve the
     // quantization type for the new section.
     if (current_section_id != embedding_hit->basic_hit().section_id()) {
@@ -151,17 +173,16 @@
       // current_allowed_sections_mask_ should be assigned to kSectionIdMaskNone
       // by AdvanceToNextEmbeddingHit, and the embedding hit should have been
       // skipped above.
-      ICING_ASSIGN_OR_RETURN(
-          quantization_type,
-          schema_store_.GetQuantizationType(
-              schema_type_id_, current_section_id));
+      ICING_ASSIGN_OR_RETURN(quantization_type,
+                             schema_store_.GetQuantizationType(
+                                 schema_type_id_, current_section_id));
     }
 
     // Calculate the semantic score.
-    ICING_ASSIGN_OR_RETURN(
-        float semantic_score,
-        embedding_index_.ScoreEmbeddingHit(*embedding_scorer_, query_,
-                                           *embedding_hit, quantization_type));
+    ICING_ASSIGN_OR_RETURN(float semantic_score,
+                           embedding_hit_accessor_->ScoreEmbeddingHit(
+                               *embedding_scorer_, query_, *embedding_hit,
+                               quantization_type, schema_name_hash_));
 
     // If the semantic score is within the desired score range, update
     // doc_hit_info_ and info_map_.
@@ -170,11 +191,13 @@
       if (matched_infos == nullptr) {
         matched_infos = &(info_map_[doc_hit_info_.document_id()]);
       }
-      matched_infos->AppendScore(semantic_score);
-      if (get_embedding_match_info_) {
+      ICING_RETURN_IF_ERROR(
+          matched_infos->AppendScore(global_scores_, semantic_score));
+      if (global_section_infos_ != nullptr) {
         // Add the section info for this embedding match.
-        matched_infos->AppendSectionInfo(current_section_id,
-                                         current_section_match_count);
+        ICING_RETURN_IF_ERROR(matched_infos->AppendSectionInfo(
+            *global_section_infos_, current_section_id,
+            current_section_match_count));
       }
     }
     ++current_section_match_count;
@@ -187,7 +210,7 @@
   return libtextclassifier3::Status::OK;
 }
 
-libtextclassifier3::Status DocHitInfoIteratorEmbedding::Advance() {
+libtextclassifier3::Status DocHitInfoIteratorEmbeddingV1::Advance() {
   do {
     ICING_RETURN_IF_ERROR(AdvanceToNextUnfilteredDocument());
   } while (doc_hit_info_.hit_section_ids_mask() == kSectionIdMaskNone);
diff --git a/icing/index/embed/doc-hit-info-iterator-embedding.h b/icing/index/embed/doc-hit-info-iterator-embedding-v1.h
similarity index 76%
rename from icing/index/embed/doc-hit-info-iterator-embedding.h
rename to icing/index/embed/doc-hit-info-iterator-embedding-v1.h
index ed2f972..fc7268a 100644
--- a/icing/index/embed/doc-hit-info-iterator-embedding.h
+++ b/icing/index/embed/doc-hit-info-iterator-embedding-v1.h
@@ -12,13 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef ICING_INDEX_EMBED_DOC_HIT_INFO_ITERATOR_EMBEDDING_H_
-#define ICING_INDEX_EMBED_DOC_HIT_INFO_ITERATOR_EMBEDDING_H_
+#ifndef ICING_INDEX_EMBED_DOC_HIT_INFO_ITERATOR_EMBEDDING_V1_H_
+#define ICING_INDEX_EMBED_DOC_HIT_INFO_ITERATOR_EMBEDDING_V1_H_
 
 #include <cstdint>
 #include <memory>
 #include <string>
-#include <string_view>
 #include <utility>
 #include <vector>
 
@@ -29,8 +28,8 @@
 #include "icing/index/embed/embedding-index.h"
 #include "icing/index/embed/embedding-query-results.h"
 #include "icing/index/embed/embedding-scorer.h"
-#include "icing/index/embed/posting-list-embedding-hit-accessor.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/index/iterator/document-filter-predicate.h"
 #include "icing/index/iterator/section-restrict-data.h"
 #include "icing/proto/search.pb.h"
 #include "icing/schema/schema-store.h"
@@ -41,8 +40,9 @@
 namespace icing {
 namespace lib {
 
-class DocHitInfoIteratorEmbedding
-    : public DocHitInfoIteratorHandlingSectionRestrict {
+class DocHitInfoIteratorEmbeddingV1
+    : public DocHitInfoIteratorHandlingSectionRestrict,
+      public DocHitInfoIteratorHandlingFilter {
  public:
   // Create a DocHitInfoIterator for iterating through all docs which have an
   // embedding matched with the provided query with a score in the range of
@@ -55,14 +55,17 @@
   // help of DocHitInfoIteratorHandlingSectionRestrict.
   //
   // Returns:
-  //   - a DocHitInfoIteratorEmbedding instance on success.
+  //   - a DocHitInfoIteratorEmbeddingV1 instance on success.
   //   - Any error from posting lists.
   static libtextclassifier3::StatusOr<
-      std::unique_ptr<DocHitInfoIteratorEmbedding>>
+      std::unique_ptr<DocHitInfoIteratorEmbeddingV1>>
   Create(const PropertyProto::VectorProto* query,
          SearchSpecProto::EmbeddingQueryMetricType::Code metric_type,
-         double score_low, double score_high, bool get_embedding_match_info,
+         double score_low, double score_high,
          EmbeddingQueryResults::EmbeddingQueryMatchInfoMap* info_map,
+         std::vector<double>* global_scores,
+         std::vector<EmbeddingMatchInfos::EmbeddingMatchSectionInfo>*
+             global_section_infos,
          const EmbeddingIndex* embedding_index,
          const DocumentStore* document_store, const SchemaStore* schema_store,
          int64_t current_time_ms);
@@ -74,13 +77,18 @@
         "Query suggestions for the semanticSearch function are not supported");
   }
 
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    return {};
+  }
+
   CallStats GetCallStats() const override {
     return CallStats(
         /*num_leaf_advance_calls_lite_index_in=*/num_advance_calls_,
         /*num_leaf_advance_calls_main_index_in=*/0,
         /*num_leaf_advance_calls_integer_index_in=*/0,
         /*num_leaf_advance_calls_no_index_in=*/0,
-        /*num_blocks_inspected_in=*/0);
+        /*num_blocks_inspected_in=*/0,
+        /*embedding_stats_in=*/{});
   }
 
   std::string ToString() const override { return "embedding_iterator"; }
@@ -91,14 +99,18 @@
       SectionIdMask filtering_section_mask) const override {}
 
  private:
-  explicit DocHitInfoIteratorEmbedding(
+  explicit DocHitInfoIteratorEmbeddingV1(
       const PropertyProto::VectorProto* query,
       SearchSpecProto::EmbeddingQueryMetricType::Code metric_type,
       std::unique_ptr<EmbeddingScorer> embedding_scorer, double score_low,
-      double score_high, bool get_embedding_match_info,
+      double score_high,
       EmbeddingQueryResults::EmbeddingQueryMatchInfoMap* info_map,
+      std::vector<double>* global_scores,
+      std::vector<EmbeddingMatchInfos::EmbeddingMatchSectionInfo>*
+          global_section_infos,
       const EmbeddingIndex* embedding_index,
-      std::unique_ptr<PostingListEmbeddingHitAccessor> posting_list_accessor,
+      std::unique_ptr<EmbeddingIndex::EmbeddingHitAccessor>
+          embedding_hit_accessor,
       const DocumentStore* document_store, const SchemaStore* schema_store,
       int64_t current_time_ms)
       : query_(*query),
@@ -106,14 +118,16 @@
         embedding_scorer_(std::move(embedding_scorer)),
         score_low_(score_low),
         score_high_(score_high),
-        get_embedding_match_info_(get_embedding_match_info),
         info_map_(*info_map),
+        global_scores_(*global_scores),
+        global_section_infos_(global_section_infos),
         embedding_index_(*embedding_index),
-        posting_list_accessor_(std::move(posting_list_accessor)),
+        embedding_hit_accessor_(std::move(embedding_hit_accessor)),
         cached_embedding_hits_idx_(0),
         current_allowed_sections_mask_(kSectionIdMaskAll),
         no_more_hit_(false),
         schema_type_id_(kInvalidSchemaTypeId),
+        schema_name_hash_(0),
         document_store_(*document_store),
         schema_store_(*schema_store),
         current_time_ms_(current_time_ms),
@@ -153,15 +167,16 @@
   double score_low_;
   double score_high_;
 
-  // Snippet arguments
-  bool get_embedding_match_info_;
-
   // MatchInfo map
   EmbeddingQueryResults::EmbeddingQueryMatchInfoMap& info_map_;  // Does not own
+  std::vector<double>& global_scores_;                           // Does not own
+  // Nullable, and does not own. If null, section info will not be populated.
+  std::vector<EmbeddingMatchInfos::EmbeddingMatchSectionInfo>*
+      global_section_infos_;
 
   // Access to embeddings index data
   const EmbeddingIndex& embedding_index_;
-  std::unique_ptr<PostingListEmbeddingHitAccessor> posting_list_accessor_;
+  std::unique_ptr<EmbeddingIndex::EmbeddingHitAccessor> embedding_hit_accessor_;
 
   // Cached data from the embeddings index
   std::vector<EmbeddingHit> cached_embedding_hits_;
@@ -169,6 +184,10 @@
   SectionIdMask current_allowed_sections_mask_;
   bool no_more_hit_;
   SchemaTypeId schema_type_id_;  // The schema type id for the current document.
+  // The schema name hash for the current document, which will be updated
+  // together with schema_type_id_ when advancing to the next document. This
+  // value is valid only if schema_type_id_ is valid.
+  uint32_t schema_name_hash_;
 
   const DocumentStore& document_store_;
   const SchemaStore& schema_store_;
@@ -179,4 +198,4 @@
 }  // namespace lib
 }  // namespace icing
 
-#endif  // ICING_INDEX_EMBED_DOC_HIT_INFO_ITERATOR_EMBEDDING_H_
+#endif  // ICING_INDEX_EMBED_DOC_HIT_INFO_ITERATOR_EMBEDDING_V1_H_
diff --git a/icing/index/embed/doc-hit-info-iterator-embedding-v2.cc b/icing/index/embed/doc-hit-info-iterator-embedding-v2.cc
new file mode 100644
index 0000000..de16bf8
--- /dev/null
+++ b/icing/index/embed/doc-hit-info-iterator-embedding-v2.cc
@@ -0,0 +1,313 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/index/embed/doc-hit-info-iterator-embedding-v2.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <memory>
+#include <numeric>
+#include <utility>
+#include <vector>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/absl_ports/canonical_errors.h"
+#include "icing/index/embed/embedding-hit.h"
+#include "icing/index/embed/embedding-index.h"
+#include "icing/index/embed/embedding-query-results.h"
+#include "icing/index/embed/embedding-scorer.h"
+#include "icing/index/hit/doc-hit-info.h"
+#include "icing/index/hit/hit.h"
+#include "icing/proto/search.pb.h"
+#include "icing/schema/schema-store.h"
+#include "icing/schema/section.h"
+#include "icing/store/document-filter-data.h"
+#include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
+#include "icing/util/status-macros.h"
+
+namespace icing {
+namespace lib {
+
+libtextclassifier3::StatusOr<std::unique_ptr<DocHitInfoIteratorEmbeddingV2>>
+DocHitInfoIteratorEmbeddingV2::Create(
+    const PropertyProto::VectorProto* query,
+    SearchSpecProto::EmbeddingQueryMetricType::Code metric_type,
+    double score_low, double score_high,
+    EmbeddingQueryResults::EmbeddingQueryMatchInfoMap* info_map,
+    std::vector<double>* global_scores,
+    std::vector<EmbeddingMatchInfos::EmbeddingMatchSectionInfo>*
+        global_section_infos,
+    const EmbeddingIndex* embedding_index, const DocumentStore* document_store,
+    const SchemaStore* schema_store, int64_t current_time_ms) {
+  ICING_RETURN_ERROR_IF_NULL(query);
+  ICING_RETURN_ERROR_IF_NULL(embedding_index);
+  ICING_RETURN_ERROR_IF_NULL(info_map);
+  ICING_RETURN_ERROR_IF_NULL(global_scores);
+  ICING_RETURN_ERROR_IF_NULL(document_store);
+  ICING_RETURN_ERROR_IF_NULL(schema_store);
+
+  libtextclassifier3::StatusOr<
+      std::unique_ptr<EmbeddingIndex::EmbeddingHitAccessor>>
+      embedding_hit_accessor_or = embedding_index->GetAccessorForVector(*query);
+  std::unique_ptr<EmbeddingIndex::EmbeddingHitAccessor> embedding_hit_accessor;
+  if (embedding_hit_accessor_or.ok()) {
+    embedding_hit_accessor = std::move(embedding_hit_accessor_or).ValueOrDie();
+  } else if (absl_ports::IsNotFound(embedding_hit_accessor_or.status())) {
+    // A not-found error should be fine, since that means there is no matching
+    // embedding hits in the index.
+    embedding_hit_accessor = nullptr;
+  } else {
+    // Otherwise, return the error as is.
+    return embedding_hit_accessor_or.status();
+  }
+
+  ICING_ASSIGN_OR_RETURN(std::unique_ptr<EmbeddingScorer> embedding_scorer,
+                         EmbeddingScorer::Create(metric_type));
+
+  return std::unique_ptr<DocHitInfoIteratorEmbeddingV2>(
+      new DocHitInfoIteratorEmbeddingV2(
+          query, metric_type, std::move(embedding_scorer), score_low,
+          score_high, info_map, global_scores, global_section_infos,
+          embedding_index, std::move(embedding_hit_accessor), document_store,
+          schema_store, current_time_ms));
+}
+
+libtextclassifier3::Status
+DocHitInfoIteratorEmbeddingV2::RetrieveNextHitsBatch() {
+  ICING_ASSIGN_OR_RETURN(std::vector<EmbeddingHit> embedding_hits,
+                         embedding_hit_accessor_->GetNextHitsBatch());
+  if (embedding_hits.empty()) {
+    no_more_hit_ = true;
+  }
+
+  // Create a sequential access order of the embedding hits for optimized
+  // memory access for embedding data.
+  cached_hit_scores_idx_ = 0;
+  cached_hit_scores_.clear();
+  cached_hit_scores_.resize(embedding_hits.size());
+  cached_section_id_masks_.clear();
+  if (delegate_ != nullptr) {
+    cached_section_id_masks_.resize(embedding_hits.size());
+  }
+
+  std::vector<int> access_order(embedding_hits.size());
+  std::iota(access_order.begin(), access_order.end(), 0);
+  std::sort(access_order.begin(), access_order.end(),
+            [&embedding_hits](int i, int j) {
+              return embedding_hits[i].location() <
+                     embedding_hits[j].location();
+            });
+  if (delegate_ != nullptr) {
+    // Add to access_order and filter any docids that don't match the delegate.
+    for (int i = 0; i < embedding_hits.size(); ++i) {
+      while (delegate_->doc_hit_info().document_id() == kInvalidDocumentId ||
+             delegate_->doc_hit_info().document_id() >
+                 embedding_hits[i].basic_hit().document_id()) {
+        if (!delegate_->Advance().ok()) {
+          break;
+        }
+      }
+      SectionIdMask section_id_mask = kSectionIdMaskNone;
+      if (delegate_->doc_hit_info().document_id() ==
+          embedding_hits[i].basic_hit().document_id()) {
+        section_id_mask = delegate_->doc_hit_info().hit_section_ids_mask();
+      }
+      cached_section_id_masks_[i] = section_id_mask;
+    }
+  }
+
+  DocumentId document_id = kInvalidDocumentId;
+  SchemaTypeId schema_type_id = kInvalidSchemaTypeId;
+  uint32_t schema_name_hash = 0;
+  SectionIdMask allowed_sections_mask = kSectionIdMaskNone;
+  for (int i = 0; i < access_order.size(); ++i) {
+    const EmbeddingHit& embedding_hit = embedding_hits[access_order[i]];
+    // Update document id, schema type id, and allowed sections mask for the
+    // new document.
+    if (embedding_hit.basic_hit().document_id() != document_id) {
+      document_id = embedding_hit.basic_hit().document_id();
+      schema_type_id = kInvalidSchemaTypeId;
+      schema_name_hash = 0;
+      allowed_sections_mask = kSectionIdMaskNone;
+
+      if (!DoesDocumentPassAllFilters(document_id)) {
+        // This means that the document is filtered out by the document filter
+        // predicate. Just skip the document.
+        continue;
+      }
+      if (delegate_ != nullptr &&
+          cached_section_id_masks_[access_order[i]] == kSectionIdMaskNone) {
+        // The document has been filtered out by the delegate. Therefore, there
+        // is no need to calculate the embedding score for this document.
+        continue;
+      }
+      schema_type_id =
+          document_store_.GetSchemaTypeId(document_id, current_time_ms_);
+      if (schema_type_id == kInvalidSchemaTypeId) {
+        // This means that the document is deleted or expired.
+        // No need to update allowed_sections_mask back to kSectionIdMaskNone,
+        // since we will always check schema_type_id itself.
+        continue;
+      }
+      ICING_ASSIGN_OR_RETURN(schema_name_hash,
+                             schema_store_.GetSchemaNameHash(schema_type_id));
+      allowed_sections_mask = ComputeAllowedSectionsMask(document_id);
+    }
+
+    SectionId section_id = embedding_hit.basic_hit().section_id();
+
+    // Filter out the embedding hit according to the section restriction.
+    if (((UINT64_C(1) << section_id) & allowed_sections_mask) == 0) {
+      continue;
+    }
+
+    // Filter out the embedding hit if the schema type id is invalid.
+    if (schema_type_id == kInvalidSchemaTypeId) {
+      continue;
+    }
+
+    // The schema type id is guaranteed to be valid here. Otherwise,
+    // allowed_sections_mask should be assigned to kSectionIdMaskNone, and the
+    // embedding hit should have been skipped above.
+    ICING_ASSIGN_OR_RETURN(
+        EmbeddingIndexingConfig::QuantizationType::Code quantization_type,
+        schema_store_.GetQuantizationType(schema_type_id, section_id));
+
+    // Calculate the semantic score.
+    ICING_ASSIGN_OR_RETURN(float semantic_score,
+                           embedding_hit_accessor_->ScoreEmbeddingHit(
+                               *embedding_scorer_, query_, embedding_hit,
+                               quantization_type, schema_name_hash));
+    cached_hit_scores_[access_order[i]] = {embedding_hit.basic_hit(),
+                                           semantic_score};
+  }
+
+  // Remove the embedding hits that are filtered out.
+  int hit_cnt = 0;
+  for (int i = 0; i < cached_hit_scores_.size(); ++i) {
+    const HitWithScore& hit_with_score = cached_hit_scores_[i];
+    if (hit_with_score.hit.is_valid()) {
+      cached_hit_scores_[hit_cnt] = hit_with_score;
+      if (delegate_ != nullptr && i < cached_section_id_masks_.size()) {
+        cached_section_id_masks_[hit_cnt] = cached_section_id_masks_[i];
+      }
+      ++hit_cnt;
+    }
+  }
+  cached_hit_scores_.resize(hit_cnt);
+  if (!cached_section_id_masks_.empty()) {
+    cached_section_id_masks_.resize(hit_cnt);
+  }
+  return libtextclassifier3::Status::OK;
+}
+
+libtextclassifier3::StatusOr<const DocHitInfoIteratorEmbeddingV2::HitWithScore*>
+DocHitInfoIteratorEmbeddingV2::AdvanceToNextEmbeddingHit() {
+  while (cached_hit_scores_idx_ == cached_hit_scores_.size()) {
+    ICING_RETURN_IF_ERROR(RetrieveNextHitsBatch());
+    if (no_more_hit_) {
+      return nullptr;
+    }
+  }
+  const HitWithScore& embedding_hit_score =
+      cached_hit_scores_[cached_hit_scores_idx_];
+  DocumentId document_id = embedding_hit_score.hit.document_id();
+  if (doc_hit_info_.document_id() == kInvalidDocumentId) {
+    doc_hit_info_.set_document_id(document_id);
+  } else if (doc_hit_info_.document_id() != document_id) {
+    return nullptr;
+  }
+  ++cached_hit_scores_idx_;
+  return &embedding_hit_score;
+}
+
+libtextclassifier3::Status
+DocHitInfoIteratorEmbeddingV2::AdvanceToNextUnfilteredDocument() {
+  if (no_more_hit_ || embedding_hit_accessor_ == nullptr) {
+    return absl_ports::ResourceExhaustedError(
+        "No more DocHitInfos in iterator");
+  }
+
+  doc_hit_info_ = DocHitInfo(kInvalidDocumentId, kSectionIdMaskNone);
+  EmbeddingMatchInfos* matched_infos = nullptr;
+  SectionId current_section_id = kInvalidSectionId;
+  int current_section_match_count = 0;
+
+  SectionIdMask delegate_section_id_mask = kSectionIdMaskNone;
+  while (true) {
+    ICING_ASSIGN_OR_RETURN(const HitWithScore* embedding_hit_score,
+                           AdvanceToNextEmbeddingHit());
+    if (embedding_hit_score == nullptr) {
+      // No more hits for the current document.
+      break;
+    }
+    if (delegate_ != nullptr) {
+      delegate_section_id_mask |=
+          cached_section_id_masks_[cached_hit_scores_idx_ - 1];
+    }
+
+    // We've reached a new section. Reset the match count and retrieve the
+    // quantization type for the new section.
+    if (current_section_id != embedding_hit_score->hit.section_id()) {
+      current_section_match_count = 0;
+      current_section_id = embedding_hit_score->hit.section_id();
+    }
+
+    float semantic_score = embedding_hit_score->score;
+    // If the semantic score is within the desired score range, update
+    // doc_hit_info_ and info_map_.
+    if (score_low_ <= semantic_score && semantic_score <= score_high_) {
+      doc_hit_info_.UpdateSection(embedding_hit_score->hit.section_id());
+      if (matched_infos == nullptr) {
+        matched_infos = &(info_map_[doc_hit_info_.document_id()]);
+      }
+      ICING_RETURN_IF_ERROR(
+          matched_infos->AppendScore(global_scores_, semantic_score));
+      if (global_section_infos_ != nullptr) {
+        // Add the section info for this embedding match.
+        ICING_RETURN_IF_ERROR(matched_infos->AppendSectionInfo(
+            *global_section_infos_, current_section_id,
+            current_section_match_count));
+      }
+    }
+    ++current_section_match_count;
+  }
+  if (doc_hit_info_.hit_section_ids_mask() != kSectionIdMaskNone) {
+    // The hit sections id mask is used by the caller of this function
+    // (DocHitInfoIteratorEmbeddingV2::Advance) to determine whether
+    // the current document has any hits meeting our score thresholds.
+    // Therefore, we can only merge here so long as at least one embedding hit
+    // has passed the score threshold.
+    doc_hit_info_.MergeSectionsFrom(delegate_section_id_mask);
+  }
+
+  if (doc_hit_info_.document_id() == kInvalidDocumentId) {
+    return absl_ports::ResourceExhaustedError(
+        "No more DocHitInfos in iterator");
+  }
+  return libtextclassifier3::Status::OK;
+}
+
+libtextclassifier3::Status DocHitInfoIteratorEmbeddingV2::Advance() {
+  do {
+    ICING_RETURN_IF_ERROR(AdvanceToNextUnfilteredDocument());
+  } while (doc_hit_info_.hit_section_ids_mask() == kSectionIdMaskNone);
+  ++num_advance_calls_;
+  return libtextclassifier3::Status::OK;
+}
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/index/embed/doc-hit-info-iterator-embedding-v2.h b/icing/index/embed/doc-hit-info-iterator-embedding-v2.h
new file mode 100644
index 0000000..366674c
--- /dev/null
+++ b/icing/index/embed/doc-hit-info-iterator-embedding-v2.h
@@ -0,0 +1,257 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef ICING_INDEX_EMBED_DOC_HIT_INFO_ITERATOR_EMBEDDING_V2_H_
+#define ICING_INDEX_EMBED_DOC_HIT_INFO_ITERATOR_EMBEDDING_V2_H_
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/absl_ports/canonical_errors.h"
+#include "icing/absl_ports/str_cat.h"
+#include "icing/index/embed/embedding-index.h"
+#include "icing/index/embed/embedding-query-results.h"
+#include "icing/index/embed/embedding-scorer.h"
+#include "icing/index/hit/hit.h"
+#include "icing/index/iterator/doc-hit-info-iterator-section-restrict.h"
+#include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/index/iterator/document-filter-predicate.h"
+#include "icing/index/iterator/section-restrict-data.h"
+#include "icing/proto/search.pb.h"
+#include "icing/schema/schema-store.h"
+#include "icing/schema/section.h"
+#include "icing/store/document-store.h"
+
+namespace icing {
+namespace lib {
+
+class DocHitInfoIteratorEmbeddingV2
+    : public DocHitInfoIteratorHandlingSectionRestrict,
+      public DocHitInfoIteratorHandlingFilter {
+ public:
+  // Create a DocHitInfoIterator for iterating through all docs which have an
+  // embedding matched with the provided query with a score in the range of
+  // [score_low, score_high], using the provided metric_type.
+  //
+  // The iterator will store the matched embedding scores in info_map to
+  // prepare for scoring and snippeting.
+  //
+  // The iterator will handle the section restriction logic internally with the
+  // help of DocHitInfoIteratorHandlingSectionRestrict.
+  //
+  // Returns:
+  //   - a DocHitInfoIteratorEmbeddingV2 instance on success.
+  //   - Any error from posting lists.
+  static libtextclassifier3::StatusOr<
+      std::unique_ptr<DocHitInfoIteratorEmbeddingV2>>
+  Create(const PropertyProto::VectorProto* query,
+         SearchSpecProto::EmbeddingQueryMetricType::Code metric_type,
+         double score_low, double score_high,
+         EmbeddingQueryResults::EmbeddingQueryMatchInfoMap* info_map,
+         std::vector<double>* global_scores,
+         std::vector<EmbeddingMatchInfos::EmbeddingMatchSectionInfo>*
+             global_section_infos,
+         const EmbeddingIndex* embedding_index,
+         const DocumentStore* document_store, const SchemaStore* schema_store,
+         int64_t current_time_ms);
+
+  bool CanAdoptDelegate() const override { return true; }
+
+  void AdoptDelegate(std::unique_ptr<DocHitInfoIterator> delegate,
+                     bool delegate_node_is_right_most) override {
+    delegate_ = std::move(delegate);
+    delegate_node_is_right_most_ = delegate_node_is_right_most;
+  }
+
+  bool HandleSectionRestriction(SectionRestrictData* other_data) override {
+    // Apply section restriction to delegate if we have one.
+    if (delegate_ != nullptr) {
+      delegate_ = DocHitInfoIteratorSectionRestrict::ApplyRestrictions(
+          std::move(delegate_), other_data);
+    }
+    return DocHitInfoIteratorHandlingSectionRestrict::HandleSectionRestriction(
+        other_data);
+  }
+
+  libtextclassifier3::Status Advance() override;
+
+  libtextclassifier3::StatusOr<TrimmedNode> TrimRightMostNode() && override {
+    if (delegate_ != nullptr && !delegate_node_is_right_most_) {
+      return std::move(*delegate_).TrimRightMostNode();
+    }
+    return absl_ports::InvalidArgumentError(
+        "Query suggestions for the semanticSearch function are not supported");
+  }
+
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    if (delegate_ != nullptr) {
+      return {&delegate_};
+    }
+    return {};
+  }
+
+  CallStats GetCallStats() const override {
+    CallStats call_stats(
+        /*num_leaf_advance_calls_lite_index_in=*/num_advance_calls_,
+        /*num_leaf_advance_calls_main_index_in=*/0,
+        /*num_leaf_advance_calls_integer_index_in=*/0,
+        /*num_leaf_advance_calls_no_index_in=*/0,
+        /*num_blocks_inspected_in=*/0,
+        embedding_hit_accessor_ != nullptr
+            ? embedding_hit_accessor_->GetEmbeddingStats()
+            : CallStats::EmbeddingStats{});
+    if (delegate_ != nullptr) {
+      call_stats += delegate_->GetCallStats();
+    }
+    return call_stats;
+  }
+
+  std::string ToString() const override {
+    if (delegate_ != nullptr) {
+      return absl_ports::StrCat("embedding_iterator with delegate (",
+                                delegate_->ToString(), ")");
+    }
+    return "embedding_iterator";
+  }
+
+  // PopulateMatchedTermsStats is not applicable to embedding search.
+  void PopulateMatchedTermsStats(
+      std::vector<TermMatchInfo>* matched_terms_stats,
+      SectionIdMask filtering_section_mask) const override {
+    if (delegate_ != nullptr) {
+      delegate_->PopulateMatchedTermsStats(matched_terms_stats,
+                                           filtering_section_mask);
+    }
+  }
+
+ private:
+  struct HitWithScore {
+    BasicHit hit;
+    float score;
+  };
+
+  explicit DocHitInfoIteratorEmbeddingV2(
+      const PropertyProto::VectorProto* query,
+      SearchSpecProto::EmbeddingQueryMetricType::Code metric_type,
+      std::unique_ptr<EmbeddingScorer> embedding_scorer, double score_low,
+      double score_high,
+      EmbeddingQueryResults::EmbeddingQueryMatchInfoMap* info_map,
+      std::vector<double>* global_scores,
+      std::vector<EmbeddingMatchInfos::EmbeddingMatchSectionInfo>*
+          global_section_infos,
+      const EmbeddingIndex* embedding_index,
+      std::unique_ptr<EmbeddingIndex::EmbeddingHitAccessor>
+          embedding_hit_accessor,
+      const DocumentStore* document_store, const SchemaStore* schema_store,
+      int64_t current_time_ms)
+      : query_(*query),
+        metric_type_(metric_type),
+        embedding_scorer_(std::move(embedding_scorer)),
+        score_low_(score_low),
+        score_high_(score_high),
+        info_map_(*info_map),
+        global_scores_(*global_scores),
+        global_section_infos_(global_section_infos),
+        embedding_index_(*embedding_index),
+        embedding_hit_accessor_(std::move(embedding_hit_accessor)),
+        cached_hit_scores_idx_(0),
+        no_more_hit_(false),
+        document_store_(*document_store),
+        schema_store_(*schema_store),
+        current_time_ms_(current_time_ms),
+        num_advance_calls_(0) {}
+
+  // Retrieve the next batch of embedding hits from the posting list.
+  //
+  // Hits that do not pass section restriction or document filter will be
+  // filtered out. Otherwise, the hits will be scored and added to
+  // cached_hit_scores_.
+  //
+  // Returns:
+  //   - OK, if it is able to retrieve the next batch of embedding hits.
+  //   - Any error from posting lists.
+  libtextclassifier3::Status RetrieveNextHitsBatch();
+
+  // Advance to the next embedding hit of the current document. If the current
+  // document id is kInvalidDocumentId, the method will advance to the first
+  // embedding hit of the next document and update doc_hit_info_.
+  //
+  // This method also properly updates cached_hit_scores_,
+  // cached_hit_scores_idx_, and no_more_hit_ to reflect the current
+  // state.
+  //
+  // Returns:
+  //   - a const pointer to the next embedding hit on success.
+  //   - nullptr, if there is no more hit for the current document, or no more
+  //     hit in general if the current document id is kInvalidDocumentId.
+  //   - Any error from posting lists.
+  libtextclassifier3::StatusOr<const HitWithScore*> AdvanceToNextEmbeddingHit();
+
+  // Similar to Advance(), this method advances the iterator to the next
+  // document, but it does not guarantee that the next document will have
+  // a matched embedding hit within the score range.
+  //
+  // Returns:
+  //   - OK, if it is able to advance to a new document_id.
+  //   - RESOUCE_EXHAUSTED, if we have run out of document_ids to iterate over.
+  //   - Any error from posting lists.
+  libtextclassifier3::Status AdvanceToNextUnfilteredDocument();
+
+  std::unique_ptr<DocHitInfoIterator> delegate_;
+  // Whether the delegate is a node to the right of the current node. This
+  // affects the behavior of TrimRightMostNode.
+  bool delegate_node_is_right_most_;
+
+  // Query information
+  const PropertyProto::VectorProto& query_;  // Does not own
+
+  // Scoring arguments
+  SearchSpecProto::EmbeddingQueryMetricType::Code metric_type_;
+  std::unique_ptr<EmbeddingScorer> embedding_scorer_;
+  double score_low_;
+  double score_high_;
+
+  // MatchInfo map
+  EmbeddingQueryResults::EmbeddingQueryMatchInfoMap& info_map_;  // Does not own
+  std::vector<double>& global_scores_;                           // Does not own
+  // Nullable, and does not own. If null, section info will not be populated.
+  std::vector<EmbeddingMatchInfos::EmbeddingMatchSectionInfo>*
+      global_section_infos_;
+
+  // Access to embeddings index data
+  const EmbeddingIndex& embedding_index_;
+  std::unique_ptr<EmbeddingIndex::EmbeddingHitAccessor>
+      embedding_hit_accessor_;  // Nullable.
+
+  // Cached data from the embeddings index
+  std::vector<HitWithScore> cached_hit_scores_;
+  std::vector<SectionIdMask> cached_section_id_masks_;
+  int cached_hit_scores_idx_;
+  bool no_more_hit_;
+
+  const DocumentStore& document_store_;
+  const SchemaStore& schema_store_;
+  int64_t current_time_ms_;
+  int num_advance_calls_;
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_INDEX_EMBED_DOC_HIT_INFO_ITERATOR_EMBEDDING_V2_H_
diff --git a/icing/index/embed/embedding-index.cc b/icing/index/embed/embedding-index.cc
index 028d85a..f05b9d1 100644
--- a/icing/index/embed/embedding-index.cc
+++ b/icing/index/embed/embedding-index.cc
@@ -17,6 +17,7 @@
 #include <algorithm>
 #include <cstdint>
 #include <cstring>
+#include <limits>
 #include <memory>
 #include <string>
 #include <string_view>
@@ -59,15 +60,17 @@
 // The maximum size of the embedding hit list mmapper.
 // We use 64MiB for 32-bit platforms and 128MiB for 64-bit platforms.
 #ifdef ICING_ARCH_BIT_64
-  constexpr uint32_t kEmbeddingHitListMapperMaxSize = 128 * 1024 * 1024;
+constexpr uint32_t kEmbeddingHitListMapperMaxSize = 128 * 1024 * 1024;
 #else
-  constexpr uint32_t kEmbeddingHitListMapperMaxSize = 64 * 1024 * 1024;
+constexpr uint32_t kEmbeddingHitListMapperMaxSize = 64 * 1024 * 1024;
 #endif
 
 // The maximum length returned by encode_util::EncodeIntToCString is 5 for
 // uint32_t.
 constexpr uint32_t kEncodedDimensionLength = 5;
 
+constexpr uint32_t kInvalidShardId = std::numeric_limits<uint32_t>::max();
+
 std::string GetMetadataFilePath(std::string_view working_path) {
   return absl_ports::StrCat(working_path, "/metadata");
 }
@@ -80,13 +83,28 @@
   return absl_ports::StrCat(working_path, "/embedding_hit_list_mapper");
 }
 
-std::string GetEmbeddingVectorsFilePath(std::string_view working_path) {
-  return absl_ports::StrCat(working_path, "/embedding_vectors");
+std::string AppendShardId(std::string&& file_path, uint32_t shard_id,
+                          uint32_t num_shards) {
+  if (num_shards > 1) {
+    return absl_ports::StrCat(std::move(file_path), "_",
+                              std::to_string(shard_id));
+  }
+  return file_path;
 }
 
-std::string GetQuantizedEmbeddingVectorsFilePath(
-    std::string_view working_path) {
-  return absl_ports::StrCat(working_path, "/quantized_embedding_vectors");
+std::string GetEmbeddingVectorsFilePath(std::string_view working_path,
+                                        uint32_t shard_id,
+                                        uint32_t num_shards) {
+  return AppendShardId(absl_ports::StrCat(working_path, "/embedding_vectors"),
+                       shard_id, num_shards);
+}
+
+std::string GetQuantizedEmbeddingVectorsFilePath(std::string_view working_path,
+                                                 uint32_t shard_id,
+                                                 uint32_t num_shards) {
+  return AppendShardId(
+      absl_ports::StrCat(working_path, "/quantized_embedding_vectors"),
+      shard_id, num_shards);
 }
 
 // An injective function that maps the ordered pair (dimension, model_signature)
@@ -126,24 +144,33 @@
 
 }  // namespace
 
+uint32_t EmbeddingIndex::GetShardId(uint32_t dimension,
+                                    std::string_view model_signature,
+                                    std::string_view schema_name) const {
+  return GetShardId(
+      GetPostingListKeyHash(GetPostingListKey(dimension, model_signature)),
+      SchemaStore::GetSchemaNameHash(schema_name));
+}
+
 libtextclassifier3::StatusOr<std::unique_ptr<EmbeddingIndex>>
 EmbeddingIndex::Create(const Filesystem* filesystem, std::string working_path,
-                       const Clock* clock, const FeatureFlags* feature_flags) {
+                       const Clock* clock, const FeatureFlags* feature_flags,
+                       uint32_t num_shards) {
   ICING_RETURN_ERROR_IF_NULL(filesystem);
   ICING_RETURN_ERROR_IF_NULL(clock);
 
-  std::unique_ptr<EmbeddingIndex> index =
-      std::unique_ptr<EmbeddingIndex>(new EmbeddingIndex(
-          *filesystem, std::move(working_path), clock, feature_flags));
+  if (num_shards == 0) {
+    return absl_ports::InvalidArgumentError("Number of shards cannot be 0");
+  }
+
+  std::unique_ptr<EmbeddingIndex> index = std::unique_ptr<EmbeddingIndex>(
+      new EmbeddingIndex(*filesystem, std::move(working_path), clock,
+                         feature_flags, num_shards));
   ICING_RETURN_IF_ERROR(index->Initialize());
   return index;
 }
 
-libtextclassifier3::Status EmbeddingIndex::CreateStorageDataIfNonEmpty() {
-  if (is_empty()) {
-    return libtextclassifier3::Status::OK;
-  }
-
+libtextclassifier3::Status EmbeddingIndex::CreateStorageData() {
   ICING_ASSIGN_OR_RETURN(FlashIndexStorage flash_index_storage,
                          FlashIndexStorage::Create(
                              GetFlashIndexStorageFilePath(working_path_),
@@ -157,17 +184,21 @@
           filesystem_, GetEmbeddingHitListMapperPath(working_path_),
           kEmbeddingHitListMapperMaxSize));
 
-  ICING_ASSIGN_OR_RETURN(
-      embedding_vectors_,
-      FileBackedVector<float>::Create(
-          filesystem_, GetEmbeddingVectorsFilePath(working_path_),
-          MemoryMappedFile::READ_WRITE_AUTO_SYNC));
-
-  ICING_ASSIGN_OR_RETURN(
-      quantized_embedding_vectors_,
-      FileBackedVector<char>::Create(
-          filesystem_, GetQuantizedEmbeddingVectorsFilePath(working_path_),
-          MemoryMappedFile::READ_WRITE_AUTO_SYNC));
+  // Scan for existing sharded vector files and load them.
+  for (int i = 0; i < num_shards_; ++i) {
+    // Non-quantized vectors
+    if (filesystem_.FileExists(
+            GetEmbeddingVectorsFilePath(working_path_, i, num_shards_)
+                .c_str())) {
+      ICING_RETURN_IF_ERROR(GetOrCreateEmbeddingVector(i));
+    }
+    // Quantized vectors
+    if (filesystem_.FileExists(
+            GetQuantizedEmbeddingVectorsFilePath(working_path_, i, num_shards_)
+                .c_str())) {
+      ICING_RETURN_IF_ERROR(GetOrCreateQuantizedEmbeddingVector(i));
+    }
+  }
 
   return libtextclassifier3::Status::OK;
 }
@@ -176,8 +207,43 @@
   if (!is_empty()) {
     return libtextclassifier3::Status::OK;
   }
+  ICING_RETURN_IF_ERROR(CreateStorageData());
   info().is_empty = false;
-  return CreateStorageDataIfNonEmpty();
+  return libtextclassifier3::Status::OK;
+}
+
+libtextclassifier3::StatusOr<FileBackedVector<float>*>
+EmbeddingIndex::GetOrCreateEmbeddingVector(uint32_t shard_id) {
+  if (shard_id >= num_shards_) {
+    return absl_ports::InvalidArgumentError("Shard id is out of range.");
+  }
+  auto& fbv_ptr = embedding_vectors_[shard_id];
+  if (fbv_ptr == nullptr) {
+    ICING_ASSIGN_OR_RETURN(
+        fbv_ptr,
+        FileBackedVector<float>::Create(
+            filesystem_,
+            GetEmbeddingVectorsFilePath(working_path_, shard_id, num_shards_),
+            MemoryMappedFile::READ_WRITE_AUTO_SYNC));
+  }
+  return fbv_ptr.get();
+}
+
+libtextclassifier3::StatusOr<FileBackedVector<char>*>
+EmbeddingIndex::GetOrCreateQuantizedEmbeddingVector(uint32_t shard_id) {
+  if (shard_id >= num_shards_) {
+    return absl_ports::InvalidArgumentError("Shard id is out of range.");
+  }
+  auto& fbv_ptr = quantized_embedding_vectors_[shard_id];
+  if (fbv_ptr == nullptr) {
+    ICING_ASSIGN_OR_RETURN(fbv_ptr,
+                           FileBackedVector<char>::Create(
+                               filesystem_,
+                               GetQuantizedEmbeddingVectorsFilePath(
+                                   working_path_, shard_id, num_shards_),
+                               MemoryMappedFile::READ_WRITE_AUTO_SYNC));
+  }
+  return fbv_ptr.get();
 }
 
 libtextclassifier3::Status EmbeddingIndex::Initialize() {
@@ -207,7 +273,8 @@
     info().magic = Info::kMagic;
     info().last_added_document_id = kInvalidDocumentId;
     info().is_empty = true;
-    memset(Info().padding_, 0, Info::kPaddingSize);
+    info().num_shards = num_shards_;
+    memset(info().padding_, 0, Info::kPaddingSize);
     ICING_RETURN_IF_ERROR(InitializeNewStorage());
   } else {
     if (metadata_mmapped_file_->available_size() != kMetadataFileSize) {
@@ -215,10 +282,39 @@
           "Incorrect metadata file size");
     }
     if (info().magic != Info::kMagic) {
-      return absl_ports::FailedPreconditionError("Incorrect magic value");
+      ICING_LOG(ERROR) << "Invalid header magic for EmbeddingIndex "
+                       << working_path_ << ". Expected: " << Info::kMagic
+                       << ", actual: " << info().magic;
+      return absl_ports::FailedPreconditionError(absl_ports::StrCat(
+          "Invalid header magic for EmbeddingIndex: ", working_path_));
     }
-    ICING_RETURN_IF_ERROR(CreateStorageDataIfNonEmpty());
+    uint32_t num_shards_read = info().num_shards;
+    // This happens for old versions of embedding index that did not have
+    // num_shards set. Just treat it as 1.
+    if (num_shards_read == 0) {
+      ICING_LOG(INFO)
+          << "Number of shards not set in metadata. Defaulting to 1.";
+      num_shards_read = 1;
+    }
+    // If num_shards doesn't match, the index need to be rebuilt.
+    if (num_shards_read != num_shards_) {
+      ICING_LOG(ERROR) << "Mismatched number of shards. Expected "
+                       << num_shards_ << ", actual " << num_shards_read;
+      return absl_ports::FailedPreconditionError(absl_ports::StrCat(
+          "Mismatched number of shards in metadata for EmbeddingIndex: ",
+          working_path_));
+    }
+    if (!info().is_empty) {
+      ICING_RETURN_IF_ERROR(CreateStorageData());
+    }
     ICING_RETURN_IF_ERROR(InitializeExistingStorage());
+    if (info().num_shards == 0) {
+      // This means that num_shards isn't set in the header, but we are still
+      // considering it a match for num_shards_. The only possibility is that
+      // num_shards_ == 1 and we have treated info().num_shards as 1.
+      // Now, update the header to record the correct num_shards_.
+      info().num_shards = num_shards_;
+    }
   }
   return libtextclassifier3::Status::OK;
 }
@@ -228,8 +324,10 @@
   metadata_mmapped_file_.reset();
   flash_index_storage_.reset();
   embedding_posting_list_mapper_.reset();
-  embedding_vectors_.reset();
-  quantized_embedding_vectors_.reset();
+  for (int i = 0; i < num_shards_; ++i) {
+    embedding_vectors_[i].reset();
+    quantized_embedding_vectors_[i].reset();
+  }
   if (filesystem_.DirectoryExists(working_path_.c_str())) {
     ICING_RETURN_IF_ERROR(Discard(filesystem_, working_path_));
   }
@@ -237,7 +335,8 @@
   return Initialize();
 }
 
-libtextclassifier3::StatusOr<std::unique_ptr<PostingListEmbeddingHitAccessor>>
+libtextclassifier3::StatusOr<
+    std::unique_ptr<EmbeddingIndex::EmbeddingHitAccessor>>
 EmbeddingIndex::GetAccessor(uint32_t dimension,
                             std::string_view model_signature) const {
   if (dimension == 0) {
@@ -250,24 +349,33 @@
   std::string key = GetPostingListKey(dimension, model_signature);
   ICING_ASSIGN_OR_RETURN(PostingListIdentifier posting_list_id,
                          embedding_posting_list_mapper_->Get(key));
-  return PostingListEmbeddingHitAccessor::CreateFromExisting(
-      flash_index_storage_.get(), posting_list_hit_serializer_.get(),
-      posting_list_id);
+  ICING_ASSIGN_OR_RETURN(
+      std::unique_ptr<PostingListEmbeddingHitAccessor> pl_accessor,
+      PostingListEmbeddingHitAccessor::CreateFromExisting(
+          flash_index_storage_.get(), posting_list_hit_serializer_.get(),
+          posting_list_id));
+  return std::make_unique<EmbeddingHitAccessor>(this, std::move(pl_accessor),
+                                                key);
 }
 
 libtextclassifier3::StatusOr<uint32_t> EmbeddingIndex::AppendEmbeddingVector(
     const PropertyProto::VectorProto& vector,
-    EmbeddingIndexingConfig::QuantizationType::Code quantization_type) {
+    EmbeddingIndexingConfig::QuantizationType::Code quantization_type,
+    uint32_t shard_id) {
   uint32_t dimension = vector.values().size();
   uint32_t location;
   if (!feature_flags_->enable_embedding_quantization() ||
       quantization_type == EmbeddingIndexingConfig::QuantizationType::NONE) {
-    location = embedding_vectors_->num_elements();
+    ICING_ASSIGN_OR_RETURN(FileBackedVector<float> * fbv_ptr,
+                           GetOrCreateEmbeddingVector(shard_id));
+    location = fbv_ptr->num_elements();
     ICING_ASSIGN_OR_RETURN(
         FileBackedVector<float>::MutableArrayView mutable_arr,
-        embedding_vectors_->Allocate(dimension));
+        fbv_ptr->Allocate(dimension));
     mutable_arr.SetArray(/*idx=*/0, vector.values().data(), dimension);
   } else {
+    ICING_ASSIGN_OR_RETURN(FileBackedVector<char> * fbv_ptr,
+                           GetOrCreateQuantizedEmbeddingVector(shard_id));
     ICING_ASSIGN_OR_RETURN(Quantizer quantizer, CreateQuantizer(vector));
     // Quantize the vector
     std::vector<uint8_t> quantized_values;
@@ -277,10 +385,9 @@
     }
 
     // Store the quantizer and the quantized vector
-    location = quantized_embedding_vectors_->num_elements();
-    ICING_ASSIGN_OR_RETURN(
-        FileBackedVector<char>::MutableArrayView mutable_arr,
-        quantized_embedding_vectors_->Allocate(sizeof(Quantizer) + dimension));
+    location = fbv_ptr->num_elements();
+    ICING_ASSIGN_OR_RETURN(FileBackedVector<char>::MutableArrayView mutable_arr,
+                           fbv_ptr->Allocate(sizeof(Quantizer) + dimension));
     mutable_arr.SetArray(/*idx=*/0, reinterpret_cast<char*>(&quantizer),
                          sizeof(Quantizer));
     mutable_arr.SetArray(/*idx=*/sizeof(Quantizer),
@@ -292,15 +399,19 @@
 
 libtextclassifier3::Status EmbeddingIndex::BufferEmbedding(
     const BasicHit& basic_hit, const PropertyProto::VectorProto& vector,
-    EmbeddingIndexingConfig::QuantizationType::Code quantization_type) {
+    EmbeddingIndexingConfig::QuantizationType::Code quantization_type,
+    std::string_view schema_name) {
   if (vector.values().empty()) {
     return absl_ports::InvalidArgumentError("Vector dimension is 0");
   }
   ICING_RETURN_IF_ERROR(MarkIndexNonEmpty());
 
   std::string key = GetPostingListKey(vector);
-  ICING_ASSIGN_OR_RETURN(uint32_t location,
-                         AppendEmbeddingVector(vector, quantization_type));
+  uint32_t shard_id = GetShardId(GetPostingListKeyHash(key),
+                                 SchemaStore::GetSchemaNameHash(schema_name));
+  ICING_ASSIGN_OR_RETURN(
+      uint32_t location,
+      AppendEmbeddingVector(vector, quantization_type, shard_id));
 
   // Buffer the embedding hit.
   pending_embedding_hits_.push_back(
@@ -373,28 +484,33 @@
 libtextclassifier3::StatusOr<uint32_t> EmbeddingIndex::TransferEmbeddingVector(
     const EmbeddingHit& old_hit, uint32_t dimension,
     EmbeddingIndexingConfig::QuantizationType::Code quantization_type,
-    EmbeddingIndex* new_index) const {
+    uint32_t shard_id, EmbeddingIndex* new_index) const {
   uint32_t new_location;
   if (!feature_flags_->enable_embedding_quantization() ||
       quantization_type == EmbeddingIndexingConfig::QuantizationType::NONE) {
     ICING_ASSIGN_OR_RETURN(const float* old_vector,
-                           GetEmbeddingVector(old_hit, dimension));
-    new_location = new_index->embedding_vectors_->num_elements();
+                           GetEmbeddingVector(old_hit, dimension, shard_id));
+
+    // Copy the embedding vector of the hit to the new index.
+    ICING_ASSIGN_OR_RETURN(FileBackedVector<float> * fbv_ptr,
+                           new_index->GetOrCreateEmbeddingVector(shard_id));
+    new_location = fbv_ptr->num_elements();
+    ICING_ASSIGN_OR_RETURN(
+        FileBackedVector<float>::MutableArrayView mutable_arr,
+        fbv_ptr->Allocate(dimension));
+    mutable_arr.SetArray(/*idx=*/0, old_vector, dimension);
+  } else {
+    ICING_ASSIGN_OR_RETURN(
+        const char* old_data,
+        GetQuantizedEmbeddingVector(old_hit, dimension, shard_id));
 
     // Copy the embedding vector of the hit to the new index.
     ICING_ASSIGN_OR_RETURN(
-        FileBackedVector<float>::MutableArrayView mutable_arr,
-        new_index->embedding_vectors_->Allocate(dimension));
-    mutable_arr.SetArray(/*idx=*/0, old_vector, dimension);
-  } else {
-    ICING_ASSIGN_OR_RETURN(const char* old_data,
-                           GetQuantizedEmbeddingVector(old_hit, dimension));
-    new_location = new_index->quantized_embedding_vectors_->num_elements();
-
-    // Copy the embedding vector of the hit to the new index.
+        FileBackedVector<char> * fbv_ptr,
+        new_index->GetOrCreateQuantizedEmbeddingVector(shard_id));
+    new_location = fbv_ptr->num_elements();
     ICING_ASSIGN_OR_RETURN(FileBackedVector<char>::MutableArrayView mutable_arr,
-                           new_index->quantized_embedding_vectors_->Allocate(
-                               sizeof(Quantizer) + dimension));
+                           fbv_ptr->Allocate(sizeof(Quantizer) + dimension));
     mutable_arr.SetArray(/*idx=*/0, old_data, sizeof(Quantizer) + dimension);
   }
   return new_location;
@@ -431,6 +547,7 @@
             /*existing_posting_list_id=*/itr->GetValue()));
     DocumentId last_new_document_id = kInvalidDocumentId;
     SchemaTypeId schema_type_id = kInvalidSchemaTypeId;
+    uint32_t shard_id = kInvalidShardId;
     while (true) {
       ICING_ASSIGN_OR_RETURN(std::vector<EmbeddingHit> batch,
                              old_pl_accessor->GetNextHitsBatch());
@@ -459,9 +576,22 @@
         if (new_document_id != last_new_document_id) {
           schema_type_id =
               document_store.GetSchemaTypeId(new_document_id, current_time_ms);
+          libtextclassifier3::StatusOr<uint32_t> schema_name_hash_or =
+              schema_store.GetSchemaNameHash(schema_type_id);
+          // Shard id only depends on posting list key and schema name hash.
+          // Posting list key will not change in this scope, so we only update
+          // the shard id here for a new document, since it can have a
+          // different schema type.
+          if (schema_name_hash_or.ok()) {
+            shard_id = GetShardId(GetPostingListKeyHash(key),
+                                  schema_name_hash_or.ValueOrDie());
+          } else {
+            shard_id = kInvalidShardId;
+          }
         }
         last_new_document_id = new_document_id;
-        if (schema_type_id == kInvalidSchemaTypeId) {
+        if (schema_type_id == kInvalidSchemaTypeId ||
+            shard_id == kInvalidShardId) {
           // This should not happen, since document store is optimized first,
           // so that new_document_id here should be alive.
           continue;
@@ -475,7 +605,7 @@
         ICING_ASSIGN_OR_RETURN(
             uint32_t new_location,
             TransferEmbeddingVector(old_hit, dimension, quantization_type,
-                                    new_index));
+                                    shard_id, new_index));
         new_hits.push_back(EmbeddingHit(
             BasicHit(old_hit.basic_hit().section_id(), new_document_id),
             new_location));
@@ -540,7 +670,7 @@
     ICING_ASSIGN_OR_RETURN(
         std::unique_ptr<EmbeddingIndex> new_index,
         EmbeddingIndex::Create(&filesystem_, temporary_index_dir.dir(), &clock_,
-                               feature_flags_));
+                               feature_flags_, num_shards_));
     ICING_RETURN_IF_ERROR(TransferIndex(*document_store, *schema_store,
                                         document_id_old_to_new,
                                         new_index.get()));
@@ -552,8 +682,10 @@
   metadata_mmapped_file_.reset();
   flash_index_storage_.reset();
   embedding_posting_list_mapper_.reset();
-  embedding_vectors_.reset();
-  quantized_embedding_vectors_.reset();
+  for (int i = 0; i < num_shards_; ++i) {
+    embedding_vectors_[i].reset();
+    quantized_embedding_vectors_[i].reset();
+  }
 
   if (!filesystem_.SwapFiles(temporary_index_dir.dir().c_str(),
                              working_path_.c_str())) {
@@ -566,28 +698,55 @@
   return Initialize();
 }
 
-libtextclassifier3::StatusOr<float> EmbeddingIndex::ScoreEmbeddingHit(
+libtextclassifier3::StatusOr<float>
+EmbeddingIndex::EmbeddingHitAccessor::ScoreEmbeddingHit(
     const EmbeddingScorer& scorer, const PropertyProto::VectorProto& query,
     const EmbeddingHit& hit,
-    EmbeddingIndexingConfig::QuantizationType::Code quantization_type) const {
+    EmbeddingIndexingConfig::QuantizationType::Code quantization_type,
+    uint32_t schema_name_hash) {
+  uint32_t shard_id =
+      embedding_index_.GetShardId(posting_list_key_hash_, schema_name_hash);
   int dimension = query.values().size();
   float semantic_score;
-  if (!feature_flags_->enable_embedding_quantization() ||
+  if (!embedding_index_.feature_flags_->enable_embedding_quantization() ||
       quantization_type == EmbeddingIndexingConfig::QuantizationType::NONE) {
-    ICING_ASSIGN_OR_RETURN(const float* vector,
-                           GetEmbeddingVector(hit, dimension));
-    semantic_score = scorer.Score(dimension,
-                                  /*v1=*/query.values().data(),
-                                  /*v2=*/vector);
+    ICING_ASSIGN_OR_RETURN(
+        const float* vector,
+        embedding_index_.GetEmbeddingVector(hit, dimension, shard_id));
+    if (embedding_index_.feature_flags_->enable_eigen_embedding_scoring()) {
+      semantic_score = scorer.EigenScore(dimension,
+                                         /*v1=*/query.values().data(),
+                                         /*v2=*/vector);
+    } else {
+      semantic_score = scorer.Score(dimension,
+                                    /*v1=*/query.values().data(),
+                                    /*v2=*/vector);
+    }
+    ++embedding_stats_.num_unquantized_embeddings_scored;
+    embedding_stats_.unquantized_shards_read.insert(shard_id);
+    embedding_stats_.num_embedding_bytes_read +=
+        static_cast<int64_t>(sizeof(float)) * dimension;
   } else {
-    ICING_ASSIGN_OR_RETURN(const char* data,
-                           GetQuantizedEmbeddingVector(hit, dimension));
+    ICING_ASSIGN_OR_RETURN(
+        const char* data,
+        embedding_index_.GetQuantizedEmbeddingVector(hit, dimension, shard_id));
     Quantizer quantizer(data);
     const uint8_t* quantized_vector =
         reinterpret_cast<const uint8_t*>(data + sizeof(Quantizer));
-    semantic_score = scorer.Score(dimension,
-                                  /*v1=*/query.values().data(),
-                                  /*v2=*/quantized_vector, quantizer);
+    if (embedding_index_.feature_flags_->enable_eigen_embedding_scoring()) {
+      semantic_score = scorer.EigenScore(dimension,
+                                         /*v1=*/query.values().data(),
+                                         /*v2=*/quantized_vector, quantizer);
+    } else {
+      semantic_score = scorer.Score(dimension,
+                                    /*v1=*/query.values().data(),
+                                    /*v2=*/quantized_vector, quantizer);
+    }
+    ++embedding_stats_.num_quantized_embeddings_scored;
+    embedding_stats_.quantized_shards_read.insert(shard_id);
+    embedding_stats_.num_embedding_bytes_read +=
+        static_cast<int64_t>(sizeof(Quantizer)) +
+        static_cast<int64_t>(sizeof(uint8_t)) * dimension;
   }
   return semantic_score;
 }
@@ -604,8 +763,14 @@
     return absl_ports::InternalError("Fail to persist flash index to disk");
   }
   ICING_RETURN_IF_ERROR(embedding_posting_list_mapper_->PersistToDisk());
-  ICING_RETURN_IF_ERROR(embedding_vectors_->PersistToDisk());
-  ICING_RETURN_IF_ERROR(quantized_embedding_vectors_->PersistToDisk());
+  for (int i = 0; i < num_shards_; ++i) {
+    if (embedding_vectors_[i] != nullptr) {
+      ICING_RETURN_IF_ERROR(embedding_vectors_[i]->PersistToDisk());
+    }
+    if (quantized_embedding_vectors_[i] != nullptr) {
+      ICING_RETURN_IF_ERROR(quantized_embedding_vectors_[i]->PersistToDisk());
+    }
+  }
   return libtextclassifier3::Status::OK;
 }
 
@@ -615,13 +780,21 @@
   }
   ICING_ASSIGN_OR_RETURN(Crc32 embedding_posting_list_mapper_crc,
                          embedding_posting_list_mapper_->UpdateChecksum());
-  ICING_ASSIGN_OR_RETURN(Crc32 embedding_vectors_crc,
-                         embedding_vectors_->UpdateChecksum());
-  ICING_ASSIGN_OR_RETURN(Crc32 quantized_embedding_vectors_crc,
-                         quantized_embedding_vectors_->UpdateChecksum());
-  return Crc32(embedding_posting_list_mapper_crc.Get() ^
-               embedding_vectors_crc.Get() ^
-               quantized_embedding_vectors_crc.Get());
+  uint32_t checksum = embedding_posting_list_mapper_crc.Get();
+
+  for (int i = 0; i < num_shards_; ++i) {
+    if (embedding_vectors_[i] != nullptr) {
+      ICING_ASSIGN_OR_RETURN(Crc32 crc,
+                             embedding_vectors_[i]->UpdateChecksum());
+      checksum ^= crc.Get();
+    }
+    if (quantized_embedding_vectors_[i] != nullptr) {
+      ICING_ASSIGN_OR_RETURN(Crc32 crc,
+                             quantized_embedding_vectors_[i]->UpdateChecksum());
+      checksum ^= crc.Get();
+    }
+  }
+  return Crc32(checksum);
 }
 
 libtextclassifier3::StatusOr<Crc32> EmbeddingIndex::GetStoragesChecksum()
@@ -631,12 +804,17 @@
   }
   ICING_ASSIGN_OR_RETURN(Crc32 embedding_posting_list_mapper_crc,
                          embedding_posting_list_mapper_->GetChecksum());
-  Crc32 embedding_vectors_crc = embedding_vectors_->GetChecksum();
-  Crc32 quantized_embedding_vectors_crc =
-      quantized_embedding_vectors_->GetChecksum();
-  return Crc32(embedding_posting_list_mapper_crc.Get() ^
-               embedding_vectors_crc.Get() ^
-               quantized_embedding_vectors_crc.Get());
+  uint32_t checksum = embedding_posting_list_mapper_crc.Get();
+
+  for (int i = 0; i < num_shards_; ++i) {
+    if (embedding_vectors_[i] != nullptr) {
+      checksum ^= embedding_vectors_[i]->GetChecksum().Get();
+    }
+    if (quantized_embedding_vectors_[i] != nullptr) {
+      checksum ^= quantized_embedding_vectors_[i]->GetChecksum().Get();
+    }
+  }
+  return Crc32(checksum);
 }
 
 }  // namespace lib
diff --git a/icing/index/embed/embedding-index.h b/icing/index/embed/embedding-index.h
index a2dfb7a..680e2f7 100644
--- a/icing/index/embed/embedding-index.h
+++ b/icing/index/embed/embedding-index.h
@@ -25,6 +25,7 @@
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/canonical_errors.h"
+#include "icing/absl_ports/str_cat.h"
 #include "icing/feature-flags.h"
 #include "icing/file/file-backed-vector.h"
 #include "icing/file/filesystem.h"
@@ -38,6 +39,7 @@
 #include "icing/index/embed/posting-list-embedding-hit-serializer.h"
 #include "icing/index/embed/quantizer.h"
 #include "icing/index/hit/hit.h"
+#include "icing/index/iterator/doc-hit-info-iterator.h"
 #include "icing/schema/schema-store.h"
 #include "icing/store/document-id.h"
 #include "icing/store/document-store.h"
@@ -57,8 +59,9 @@
     int32_t magic;
     DocumentId last_added_document_id;
     bool is_empty;
+    uint32_t num_shards;
 
-    static constexpr int kPaddingSize = 1000;
+    static constexpr int kPaddingSize = 996;
     // Padding exists just to reserve space for additional values.
     uint8_t padding_[kPaddingSize];
 
@@ -92,7 +95,8 @@
   //     DynamicTrieKeyMapper, or FileBackedVector.
   static libtextclassifier3::StatusOr<std::unique_ptr<EmbeddingIndex>> Create(
       const Filesystem* filesystem, std::string working_path,
-      const Clock* clock, const FeatureFlags* feature_flags);
+      const Clock* clock, const FeatureFlags* feature_flags,
+      uint32_t num_shards);
 
   static libtextclassifier3::Status Discard(const Filesystem& filesystem,
                                             const std::string& working_path) {
@@ -121,7 +125,8 @@
   //   - INTERNAL_ERROR on I/O error
   libtextclassifier3::Status BufferEmbedding(
       const BasicHit& basic_hit, const PropertyProto::VectorProto& vector,
-      EmbeddingIndexingConfig::QuantizationType::Code quantization_type);
+      EmbeddingIndexingConfig::QuantizationType::Code quantization_type,
+      std::string_view schema_name);
 
   // Commit the embedding hits in the buffer to the index.
   //
@@ -131,26 +136,66 @@
   //   - Any error from posting lists
   libtextclassifier3::Status CommitBufferToIndex();
 
-  // Returns a PostingListEmbeddingHitAccessor for all embedding hits that match
+  class EmbeddingHitAccessor {
+   public:
+    EmbeddingHitAccessor(
+        const EmbeddingIndex* embedding_index,
+        std::unique_ptr<PostingListEmbeddingHitAccessor> pl_accessor,
+        std::string_view posting_list_key)
+        : embedding_index_(*embedding_index),
+          pl_accessor_(std::move(pl_accessor)),
+          posting_list_key_hash_(
+              EmbeddingIndex::GetPostingListKeyHash(posting_list_key)) {}
+
+    libtextclassifier3::StatusOr<std::vector<EmbeddingHit>> GetNextHitsBatch() {
+      return pl_accessor_->GetNextHitsBatch();
+    }
+
+    // Calculates the score for the given embedding hit with the given query.
+    //
+    // Returns:
+    //   - The score on success.
+    //   - OUT_OF_RANGE_ERROR if the referred vector is out of range based on
+    //     the location and dimension.
+    //   - Any error from schema store when getting the quantization type.
+    libtextclassifier3::StatusOr<float> ScoreEmbeddingHit(
+        const EmbeddingScorer& scorer, const PropertyProto::VectorProto& query,
+        const EmbeddingHit& hit,
+        EmbeddingIndexingConfig::QuantizationType::Code quantization_type,
+        uint32_t schema_name_hash);
+
+    const DocHitInfoIterator::CallStats::EmbeddingStats& GetEmbeddingStats()
+        const {
+      return embedding_stats_;
+    }
+
+   private:
+    const EmbeddingIndex& embedding_index_;
+    std::unique_ptr<PostingListEmbeddingHitAccessor> pl_accessor_;
+    const uint32_t posting_list_key_hash_;
+    DocHitInfoIterator::CallStats::EmbeddingStats embedding_stats_;
+  };
+
+  // Returns a EmbeddingHitAccessor for all embedding hits that match
   // with the provided dimension and signature.
   //
   // Returns:
-  //   - a PostingListEmbeddingHitAccessor instance on success.
+  //   - a EmbeddingHitAccessor instance on success.
   //   - INVALID_ARGUMENT error if the dimension is 0.
   //   - NOT_FOUND error if there is no matching embedding hit.
   //   - Any error from posting lists.
-  libtextclassifier3::StatusOr<std::unique_ptr<PostingListEmbeddingHitAccessor>>
+  libtextclassifier3::StatusOr<std::unique_ptr<EmbeddingHitAccessor>>
   GetAccessor(uint32_t dimension, std::string_view model_signature) const;
 
-  // Returns a PostingListEmbeddingHitAccessor for all embedding hits that match
+  // Returns a EmbeddingHitAccessor for all embedding hits that match
   // with the provided vector's dimension and signature.
   //
   // Returns:
-  //   - a PostingListEmbeddingHitAccessor instance on success.
+  //   - a EmbeddingHitAccessor instance on success.
   //   - INVALID_ARGUMENT error if the dimension is 0.
   //   - NOT_FOUND error if there is no matching embedding hit.
   //   - Any error from posting lists.
-  libtextclassifier3::StatusOr<std::unique_ptr<PostingListEmbeddingHitAccessor>>
+  libtextclassifier3::StatusOr<std::unique_ptr<EmbeddingHitAccessor>>
   GetAccessorForVector(const PropertyProto::VectorProto& vector) const {
     return GetAccessor(vector.values_size(), vector.model_signature());
   }
@@ -172,62 +217,69 @@
   //
   // Returns:
   //   - a pointer to the embedding vector on success.
+  //   - INVALID_ARGUMENT if the shard does not exist.
   //   - OUT_OF_RANGE error if the referred vector is out of range based on the
   //     location and dimension.
   libtextclassifier3::StatusOr<const float*> GetEmbeddingVector(
-      const EmbeddingHit& hit, uint32_t dimension) const {
+      const EmbeddingHit& hit, uint32_t dimension, uint32_t shard_id) const {
+    if (shard_id >= num_shards_ || embedding_vectors_[shard_id] == nullptr) {
+      return absl_ports::InvalidArgumentError(
+          "Attempting to query a non-existent storage shard.");
+    }
+    const auto& fbv = embedding_vectors_[shard_id];
     if (static_cast<int64_t>(hit.location()) + dimension >
-        GetTotalVectorSize()) {
+        fbv->num_elements()) {
       return absl_ports::OutOfRangeError(
           "Got an embedding hit that refers to a vector out of range.");
     }
-    return embedding_vectors_->array() + hit.location();
+    return fbv->array() + hit.location();
   }
   libtextclassifier3::StatusOr<const char*> GetQuantizedEmbeddingVector(
-      const EmbeddingHit& hit, uint32_t dimension) const {
+      const EmbeddingHit& hit, uint32_t dimension, uint32_t shard_id) const {
+    if (shard_id >= num_shards_ ||
+        quantized_embedding_vectors_[shard_id] == nullptr) {
+      return absl_ports::InvalidArgumentError(
+          "Attempting to query a non-existent storage shard.");
+    }
+    const auto& fbv = quantized_embedding_vectors_[shard_id];
     // quantized_embedding_vectors_ stores data in char format. Every quantized
     // embedding vector contains a Quantizer header followed by the actual
     // vector, and every value in the vector is stored in uint8_t.
     if (static_cast<int64_t>(hit.location()) + sizeof(Quantizer) +
             sizeof(uint8_t) * dimension >
-        GetTotalQuantizedVectorSize()) {
+        fbv->num_elements()) {
       return absl_ports::OutOfRangeError(
           "Got an embedding hit that refers to a vector out of range.");
     }
-    return quantized_embedding_vectors_->array() + hit.location();
+    return fbv->array() + hit.location();
   }
 
-  // Calculates the score for the given embedding hit with the given query.
-  //
-  // Returns:
-  //   - The score on success.
-  //   - OUT_OF_RANGE_ERROR if the referred vector is out of range based on the
-  //     location and dimension.
-  //   - Any error from schema store when getting the quantization type.
-  libtextclassifier3::StatusOr<float> ScoreEmbeddingHit(
-      const EmbeddingScorer& scorer, const PropertyProto::VectorProto& query,
-      const EmbeddingHit& hit,
-      EmbeddingIndexingConfig::QuantizationType::Code quantization_type) const;
-
-  libtextclassifier3::StatusOr<const float*> GetRawEmbeddingData() const {
+  libtextclassifier3::StatusOr<const float*> GetRawEmbeddingData(
+      uint32_t shard_id) const {
     if (is_empty()) {
       return absl_ports::NotFoundError("EmbeddingIndex is empty");
     }
-    return embedding_vectors_->array();
+    if (shard_id >= num_shards_ || embedding_vectors_[shard_id] == nullptr) {
+      return absl_ports::NotFoundError(absl_ports::StrCat(
+          "Embedding storage shard ", std::to_string(shard_id), " not found"));
+    }
+    return embedding_vectors_[shard_id]->array();
   }
 
-  int32_t GetTotalVectorSize() const {
-    if (is_empty()) {
+  int32_t GetTotalVectorSize(uint32_t shard_id) const {
+    if (is_empty() || shard_id >= num_shards_ ||
+        embedding_vectors_[shard_id] == nullptr) {
       return 0;
     }
-    return embedding_vectors_->num_elements();
+    return embedding_vectors_[shard_id]->num_elements();
   }
 
-  int32_t GetTotalQuantizedVectorSize() const {
-    if (is_empty()) {
+  int32_t GetTotalQuantizedVectorSize(uint32_t shard_id) const {
+    if (is_empty() || shard_id >= num_shards_ ||
+        quantized_embedding_vectors_[shard_id] == nullptr) {
       return 0;
     }
-    return quantized_embedding_vectors_->num_elements();
+    return quantized_embedding_vectors_[shard_id]->num_elements();
   }
 
   DocumentId last_added_document_id() const {
@@ -244,32 +296,63 @@
 
   bool is_empty() const { return info().is_empty; }
 
+  uint32_t GetShardId(uint32_t dimension, std::string_view model_signature,
+                      std::string_view schema_name) const;
+
+  uint32_t num_shards() const { return num_shards_; }
+
+  Info& info() {
+    return *reinterpret_cast<Info*>(metadata_mmapped_file_->mutable_region() +
+                                    kInfoMetadataBufferOffset);
+  }
+
+  const Info& info() const {
+    return *reinterpret_cast<const Info*>(metadata_mmapped_file_->region() +
+                                          kInfoMetadataBufferOffset);
+  }
+
  private:
   explicit EmbeddingIndex(const Filesystem& filesystem,
                           std::string working_path, const Clock* clock,
-                          const FeatureFlags* feature_flags)
+                          const FeatureFlags* feature_flags,
+                          uint32_t num_shards)
       : PersistentStorage(filesystem, std::move(working_path),
                           kWorkingPathType),
         clock_(*clock),
-        feature_flags_(feature_flags) {}
+        feature_flags_(feature_flags),
+        num_shards_(num_shards),
+        embedding_vectors_(num_shards),
+        quantized_embedding_vectors_(num_shards) {}
 
-  // Creates the storage data if the index is not empty. This will initialize
-  // flash_index_storage_, embedding_posting_list_mapper_, embedding_vectors_.
+  friend class EmbeddingHitAccessor;
+
+  static uint32_t GetPostingListKeyHash(std::string_view posting_list_key) {
+    return Crc32(posting_list_key).Get();
+  }
+
+  uint32_t GetShardId(uint32_t posting_list_key_hash,
+                      uint32_t schema_name_hash) const {
+    return (posting_list_key_hash * 31 + schema_name_hash) % num_shards_;
+  }
+
+  // Creates the storage data. This will initialize flash_index_storage_,
+  // embedding_posting_list_mapper_, and scan and initialize for existing vector
+  // storage files.
   //
   // Returns:
   //   - OK on success
   //   - Any error from FlashIndexStorage, DynamicTrieKeyMapper, or
   //     FileBackedVector.
-  libtextclassifier3::Status CreateStorageDataIfNonEmpty();
+  libtextclassifier3::Status CreateStorageData();
 
   // Marks the index's header to indicate that the index is non-empty.
   //
   // If the index is already marked as non-empty, this is a no-op. Otherwise,
-  // CreateStorageDataIfNonEmpty will be called to create the storage data.
+  // CreateStorageData will be called to create the storage data.
   //
   // Returns:
   //   - OK on success
-  //   - Any error when calling CreateStorageDataIfNonEmpty.
+  //   - Any error when calling CreateStorageData.
   libtextclassifier3::Status MarkIndexNonEmpty();
 
   libtextclassifier3::Status Initialize();
@@ -283,7 +366,7 @@
   libtextclassifier3::StatusOr<uint32_t> TransferEmbeddingVector(
       const EmbeddingHit& old_hit, uint32_t dimension,
       EmbeddingIndexingConfig::QuantizationType::Code quantization_type,
-      EmbeddingIndex* new_index) const;
+      uint32_t shard_id, EmbeddingIndex* new_index) const;
 
   // Transfers embedding data and hits from the current index to new_index.
   //
@@ -317,16 +400,17 @@
   libtextclassifier3::StatusOr<Crc32> GetStoragesChecksum() const override;
 
   // Appends the given embedding vector to the appropriate vector storage
-  // (embedding_vectors_ or quantized_embedding_vectors_) based on the
-  // quantization type.
+  // shard based on the quantization type and shard_id. If the storage shard
+  // does not exist, it will be created.
   //
   // Returns:
   //   - The location of the appended vector (i.e., the starting index within
-  //     the vector storage).
+  //     the vector storage shard).
   //   - Any error when allocating the vector storage.
   libtextclassifier3::StatusOr<uint32_t> AppendEmbeddingVector(
       const PropertyProto::VectorProto& vector,
-      EmbeddingIndexingConfig::QuantizationType::Code quantization_type);
+      EmbeddingIndexingConfig::QuantizationType::Code quantization_type,
+      uint32_t shard_id);
 
   Crcs& crcs() override {
     return *reinterpret_cast<Crcs*>(metadata_mmapped_file_->mutable_region() +
@@ -338,18 +422,15 @@
                                           kCrcsMetadataBufferOffset);
   }
 
-  Info& info() {
-    return *reinterpret_cast<Info*>(metadata_mmapped_file_->mutable_region() +
-                                    kInfoMetadataBufferOffset);
-  }
+  libtextclassifier3::StatusOr<FileBackedVector<float>*>
+  GetOrCreateEmbeddingVector(uint32_t shard_id);
 
-  const Info& info() const {
-    return *reinterpret_cast<const Info*>(metadata_mmapped_file_->region() +
-                                          kInfoMetadataBufferOffset);
-  }
+  libtextclassifier3::StatusOr<FileBackedVector<char>*>
+  GetOrCreateQuantizedEmbeddingVector(uint32_t shard_id);
 
   const Clock& clock_;
   const FeatureFlags* feature_flags_;  // Does not own.
+  const uint32_t num_shards_;
 
   // In memory data:
   // Pending embedding hits with their embedding keys used for
@@ -377,11 +458,13 @@
   std::unique_ptr<KeyMapper<PostingListIdentifier>>
       embedding_posting_list_mapper_;
 
-  // A single FileBackedVector that holds all embedding vectors.
+  // An array of FileBackedVectors that hold all embedding vectors, sharded by
+  // a hash.
   //
-  // null if the index is empty.
-  std::unique_ptr<FileBackedVector<float>> embedding_vectors_;
-  std::unique_ptr<FileBackedVector<char>> quantized_embedding_vectors_;
+  // An element is null if its corresponding file does not exist.
+  std::vector<std::unique_ptr<FileBackedVector<float>>> embedding_vectors_;
+  std::vector<std::unique_ptr<FileBackedVector<char>>>
+      quantized_embedding_vectors_;
 };
 
 }  // namespace lib
diff --git a/icing/index/embed/embedding-index_test.cc b/icing/index/embed/embedding-index_test.cc
index 12ba95e..80031ec 100644
--- a/icing/index/embed/embedding-index_test.cc
+++ b/icing/index/embed/embedding-index_test.cc
@@ -20,6 +20,7 @@
 #include <memory>
 #include <string>
 #include <string_view>
+#include <unordered_set>
 #include <utility>
 #include <vector>
 
@@ -28,6 +29,7 @@
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "icing/absl_ports/canonical_errors.h"
+#include "icing/absl_ports/str_cat.h"
 #include "icing/document-builder.h"
 #include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
@@ -36,6 +38,7 @@
 #include "icing/index/embed/quantizer.h"
 #include "icing/index/hit/hit.h"
 #include "icing/legacy/index/icing-filesystem.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/proto/document.pb.h"
 #include "icing/schema-builder.h"
 #include "icing/schema/schema-store.h"
@@ -48,6 +51,7 @@
 #include "icing/testing/tmp-directory.h"
 #include "icing/util/clock.h"
 #include "icing/util/crc32.h"
+#include "icing/util/document-util.h"
 
 namespace icing {
 namespace lib {
@@ -59,12 +63,17 @@
 using ::testing::FloatNear;
 using ::testing::HasSubstr;
 using ::testing::IsEmpty;
+using ::testing::Not;
 using ::testing::Pointwise;
 using ::testing::Test;
 
 static constexpr SectionId kSectionIdQuantizedEmbedding = 2;
 static constexpr float kEpsQuantized = 0.01f;
 
+static constexpr uint32_t kDefaultDimension = 3;
+static const char kDefaultModelSignature[] = "model";
+static constexpr std::string_view kDefaultSchemaName = "type";
+
 class EmbeddingIndexTest : public Test {
  protected:
   void SetUp() override {
@@ -77,26 +86,35 @@
     filesystem_.CreateDirectoryRecursively(document_store_dir_.c_str());
     filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str());
 
+    test_vector1_ = CreateVector(kDefaultModelSignature, {0.1, 0.2, 0.3});
+    test_vector2_ = CreateVector(kDefaultModelSignature, {-0.1, -0.2, -0.3});
+    test_vector3_ = CreateVector(kDefaultModelSignature, {0.4, 0.5, 0.6});
+
     ICING_ASSERT_OK_AND_ASSIGN(
         schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
                                            &clock_, feature_flags_.get()));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, document_store_dir_, &clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, document_store_dir_, &clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
 
     ICING_ASSERT_OK_AND_ASSIGN(
         embedding_index_,
         EmbeddingIndex::Create(&filesystem_, embedding_index_dir_, &clock_,
-                               feature_flags_.get()));
+                               feature_flags_.get(),
+                               /*num_shards=*/32));
 
     ICING_ASSERT_OK(schema_store_->SetSchema(
         SchemaBuilder()
@@ -122,12 +140,15 @@
                             .SetCardinality(CARDINALITY_OPTIONAL)))
             .Build(),
         /*ignore_errors_and_delete_documents=*/false));
-    ICING_ASSERT_OK(document_store_->Put(
-        DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
-    ICING_ASSERT_OK(document_store_->Put(
-        DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
-    ICING_ASSERT_OK(document_store_->Put(
-        DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+    ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+        DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
+    ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+        DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
+    ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+        DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
+
+    default_shard_id_ = embedding_index_->GetShardId(
+        kDefaultDimension, kDefaultModelSignature, kDefaultSchemaName);
   }
 
   void TearDown() override {
@@ -157,8 +178,35 @@
   std::unique_ptr<SchemaStore> schema_store_;
   std::unique_ptr<DocumentStore> document_store_;
   std::unique_ptr<EmbeddingIndex> embedding_index_;
+
+  PropertyProto::VectorProto test_vector1_;
+  PropertyProto::VectorProto test_vector2_;
+  PropertyProto::VectorProto test_vector3_;
+
+  uint32_t default_shard_id_;
 };
 
+TEST_F(EmbeddingIndexTest, GetShardId) {
+  // Hardcode some inputs to the GetShardId function, so that we can be aware of
+  // any changes to the hashing function.
+  EXPECT_EQ(embedding_index_->GetShardId(768, "model1", "schema1"), 10);
+  EXPECT_EQ(embedding_index_->GetShardId(768, "model1", "schema2"), 4);
+  EXPECT_EQ(embedding_index_->GetShardId(768, "model2", "schema1"), 20);
+  EXPECT_EQ(embedding_index_->GetShardId(768, "model2", "schema2"), 14);
+  EXPECT_EQ(embedding_index_->GetShardId(1024, "model1", "schema1"), 27);
+  EXPECT_EQ(embedding_index_->GetShardId(1024, "model1", "schema2"), 21);
+  EXPECT_EQ(embedding_index_->GetShardId(1024, "model2", "schema1"), 1);
+  EXPECT_EQ(embedding_index_->GetShardId(1024, "model2", "schema2"), 27);
+  EXPECT_EQ(embedding_index_->GetShardId(100, "aa", "bb"), 4);
+  EXPECT_EQ(embedding_index_->GetShardId(100, "bb", "aa"), 20);
+  EXPECT_EQ(embedding_index_->GetShardId(100, "aa", "aa"), 27);
+  EXPECT_EQ(embedding_index_->GetShardId(100, "bb", "bb"), 29);
+  EXPECT_EQ(embedding_index_->GetShardId(100, "aa", "aaa"), 18);
+  EXPECT_EQ(embedding_index_->GetShardId(100, "bb", "bbb"), 11);
+  EXPECT_EQ(embedding_index_->GetShardId(100, "aaa", "aa"), 4);
+  EXPECT_EQ(embedding_index_->GetShardId(100, "bbb", "bb"), 13);
+}
+
 TEST_F(EmbeddingIndexTest, EmptyIndexContainsMetadataOnly) {
   EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(true));
 }
@@ -168,11 +216,13 @@
       GetTestTempDir() + "/embedding_index_test_local";
 
   EXPECT_THAT(EmbeddingIndex::Create(nullptr, embedding_index_dir, &clock_,
-                                     feature_flags_.get()),
+                                     feature_flags_.get(),
+                                     /*num_shards=*/32),
               StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 
   EXPECT_THAT(EmbeddingIndex::Create(&filesystem_, embedding_index_dir, nullptr,
-                                     feature_flags_.get()),
+                                     feature_flags_.get(),
+                                     /*num_shards=*/32),
               StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 }
 
@@ -184,22 +234,23 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<EmbeddingIndex> embedding_index,
       EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
-                             feature_flags_.get()));
+                             feature_flags_.get(),
+                             /*num_shards=*/32));
 
-  PropertyProto::VectorProto vector = CreateVector("model", {0.1, 0.2, 0.3});
   ICING_ASSERT_OK(embedding_index->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), test_vector1_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index->CommitBufferToIndex());
   embedding_index->set_last_added_document_id(0);
 
   EXPECT_THAT(
       GetEmbeddingHitsFromIndex(embedding_index.get(), /*dimension=*/3,
-                                /*model_signature=*/"model"),
+                                kDefaultModelSignature),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/0), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index.get()),
-              ElementsAre(0.1, 0.2, 0.3));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index.get(), default_shard_id_),
+      ElementsAre(0.1, 0.2, 0.3));
   EXPECT_EQ(embedding_index->last_added_document_id(), 0);
   // GetChecksum should succeed without updating the checksum.
   ICING_EXPECT_OK(embedding_index->GetChecksum());
@@ -207,13 +258,101 @@
   // 2. Try to create another index with the same directory. This should fail
   // due to checksum mismatch.
   EXPECT_THAT(EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
-                                     feature_flags_.get()),
+                                     feature_flags_.get(),
+                                     /*num_shards=*/32),
               StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 
   embedding_index.reset();
   filesystem_.DeleteDirectoryRecursively(embedding_index_dir.c_str());
 }
 
+TEST_F(EmbeddingIndexTest, InitializationShouldFailWithZeroShards) {
+  std::string embedding_index_dir =
+      GetTestTempDir() + "/embedding_index_test_local";
+  EXPECT_THAT(EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
+                                     feature_flags_.get(),
+                                     /*num_shards=*/0),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+TEST_F(EmbeddingIndexTest, InitializationShouldFailWithMismatchedNumShards) {
+  std::string embedding_index_dir =
+      GetTestTempDir() + "/embedding_index_test_local";
+  // 1. Create an index with num_shards = 1.
+  {
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<EmbeddingIndex> embedding_index,
+        EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
+                               feature_flags_.get(), /*num_shards=*/1));
+    ICING_ASSERT_OK(embedding_index->PersistToDisk());
+  }
+
+  // 2. Try to create another index with a different num_shards. This should
+  // fail.
+  EXPECT_THAT(EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
+                                     feature_flags_.get(),
+                                     /*num_shards=*/32),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION,
+                       HasSubstr("Mismatched number of shards")));
+
+  filesystem_.DeleteDirectoryRecursively(embedding_index_dir.c_str());
+}
+
+TEST_F(EmbeddingIndexTest,
+       InitializationShouldSucceedWithNumShardsUpgradeFromZeroToOne) {
+  std::string embedding_index_dir =
+      GetTestTempDir() + "/embedding_index_test_local";
+
+  // 1. Create an index with num_shards = 1, and manually set num_shards to 0 in
+  // the header.
+  {
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<EmbeddingIndex> embedding_index,
+        EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
+                               feature_flags_.get(), /*num_shards=*/1));
+    embedding_index->info().num_shards = 0;
+    ICING_ASSERT_OK(embedding_index->PersistToDisk());
+  }
+
+  // 2. Re-initialize with num_shards = 1. It should succeed.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<EmbeddingIndex> embedding_index,
+      EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
+                             feature_flags_.get(), /*num_shards=*/1));
+
+  // 3. Check that num_shards in the header is now 1.
+  EXPECT_EQ(embedding_index->num_shards(), 1);
+  EXPECT_EQ(embedding_index->info().num_shards, 1);
+
+  filesystem_.DeleteDirectoryRecursively(embedding_index_dir.c_str());
+}
+
+TEST_F(EmbeddingIndexTest,
+       InitializationShouldFailWithNumShardsUpgradeFromZeroToThirtyTwo) {
+  std::string embedding_index_dir =
+      GetTestTempDir() + "/embedding_index_test_local";
+
+  // 1. Create an index with num_shards = 1, and manually set num_shards to 0 in
+  // the header.
+  {
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<EmbeddingIndex> embedding_index,
+        EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
+                               feature_flags_.get(), /*num_shards=*/1));
+    embedding_index->info().num_shards = 0;
+    ICING_ASSERT_OK(embedding_index->PersistToDisk());
+  }
+
+  // 2. Re-initialize with num_shards = 32. It should fail.
+  EXPECT_THAT(EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
+                                     feature_flags_.get(),
+                                     /*num_shards=*/32),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION,
+                       HasSubstr("Mismatched number of shards")));
+
+  filesystem_.DeleteDirectoryRecursively(embedding_index_dir.c_str());
+}
+
 TEST_F(EmbeddingIndexTest, InitializationShouldSucceedWithUpdateChecksums) {
   // 1. Create index and confirm that data was properly added.
   std::string embedding_index_dir =
@@ -221,22 +360,23 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<EmbeddingIndex> embedding_index,
       EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
-                             feature_flags_.get()));
+                             feature_flags_.get(),
+                             /*num_shards=*/32));
 
-  PropertyProto::VectorProto vector = CreateVector("model", {0.1, 0.2, 0.3});
   ICING_ASSERT_OK(embedding_index->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), test_vector1_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index->CommitBufferToIndex());
   embedding_index->set_last_added_document_id(0);
 
   EXPECT_THAT(
       GetEmbeddingHitsFromIndex(embedding_index.get(), /*dimension=*/3,
-                                /*model_signature=*/"model"),
+                                kDefaultModelSignature),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/0), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index.get()),
-              ElementsAre(0.1, 0.2, 0.3));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index.get(), default_shard_id_),
+      ElementsAre(0.1, 0.2, 0.3));
   EXPECT_EQ(embedding_index->last_added_document_id(), 0);
 
   // 2. Update checksums to reflect the new content.
@@ -248,14 +388,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<EmbeddingIndex> embedding_index_two,
       EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
-                             feature_flags_.get()));
+                             feature_flags_.get(),
+                             /*num_shards=*/32));
 
   EXPECT_THAT(
       GetEmbeddingHitsFromIndex(embedding_index_two.get(), /*dimension=*/3,
-                                /*model_signature=*/"model"),
+                                kDefaultModelSignature),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/0), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_two.get()),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_two.get(),
+                                           default_shard_id_),
               ElementsAre(0.1, 0.2, 0.3));
   EXPECT_EQ(embedding_index_two->last_added_document_id(), 0);
 
@@ -271,22 +413,23 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<EmbeddingIndex> embedding_index,
       EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
-                             feature_flags_.get()));
+                             feature_flags_.get(),
+                             /*num_shards=*/32));
 
-  PropertyProto::VectorProto vector = CreateVector("model", {0.1, 0.2, 0.3});
   ICING_ASSERT_OK(embedding_index->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), test_vector1_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index->CommitBufferToIndex());
   embedding_index->set_last_added_document_id(0);
 
   EXPECT_THAT(
       GetEmbeddingHitsFromIndex(embedding_index.get(), /*dimension=*/3,
-                                /*model_signature=*/"model"),
+                                kDefaultModelSignature),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/0), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index.get()),
-              ElementsAre(0.1, 0.2, 0.3));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index.get(), default_shard_id_),
+      ElementsAre(0.1, 0.2, 0.3));
   EXPECT_EQ(embedding_index->last_added_document_id(), 0);
 
   // 2. Update checksums to reflect the new content.
@@ -296,14 +439,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<EmbeddingIndex> embedding_index_two,
       EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
-                             feature_flags_.get()));
+                             feature_flags_.get(),
+                             /*num_shards=*/32));
 
   EXPECT_THAT(
       GetEmbeddingHitsFromIndex(embedding_index_two.get(), /*dimension=*/3,
-                                /*model_signature=*/"model"),
+                                kDefaultModelSignature),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/0), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_two.get()),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_two.get(),
+                                           default_shard_id_),
               ElementsAre(0.1, 0.2, 0.3));
   EXPECT_EQ(embedding_index_two->last_added_document_id(), 0);
 
@@ -314,114 +459,109 @@
 
 TEST_F(EmbeddingIndexTest, GetEmbeddingVectorShouldFailWhenOutOfRange) {
   BasicHit basic_hit(/*section_id=*/0, /*document_id=*/0);
-  PropertyProto::VectorProto vector = CreateVector("model", {0.1, 0.2, 0.3});
-  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(basic_hit, vector,
-                                                    QUANTIZATION_TYPE_NONE));
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      basic_hit, test_vector1_, QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   EmbeddingHit embedding_hit(basic_hit, /*location=*/0);
   uint32_t dimension = 3;
-  ICING_ASSERT_OK(
-      embedding_index_->GetEmbeddingVector(embedding_hit, dimension));
-  EXPECT_THAT(
-      embedding_index_->GetEmbeddingVector(embedding_hit, dimension + 1),
-      StatusIs(libtextclassifier3::StatusCode::OUT_OF_RANGE));
+  ICING_ASSERT_OK(embedding_index_->GetEmbeddingVector(embedding_hit, dimension,
+                                                       default_shard_id_));
+  EXPECT_THAT(embedding_index_->GetEmbeddingVector(embedding_hit, dimension + 1,
+                                                   default_shard_id_),
+              StatusIs(libtextclassifier3::StatusCode::OUT_OF_RANGE));
 }
 
 TEST_F(EmbeddingIndexTest,
        GetQuantizedEmbeddingVectorShouldFailWhenOutOfRange) {
   BasicHit basic_hit(kSectionIdQuantizedEmbedding, /*document_id=*/0);
-  PropertyProto::VectorProto vector = CreateVector("model", {0.1, 0.2, 0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      basic_hit, vector, QUANTIZATION_TYPE_QUANTIZE_8_BIT));
+      basic_hit, test_vector1_, QUANTIZATION_TYPE_QUANTIZE_8_BIT,
+      kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   EmbeddingHit embedding_hit(basic_hit, /*location=*/0);
   uint32_t dimension = 3;
-  ICING_ASSERT_OK(
-      embedding_index_->GetQuantizedEmbeddingVector(embedding_hit, dimension));
-  EXPECT_THAT(embedding_index_->GetQuantizedEmbeddingVector(embedding_hit,
-                                                            dimension + 1),
+  ICING_ASSERT_OK(embedding_index_->GetQuantizedEmbeddingVector(
+      embedding_hit, dimension, default_shard_id_));
+  EXPECT_THAT(embedding_index_->GetQuantizedEmbeddingVector(
+                  embedding_hit, dimension + 1, default_shard_id_),
               StatusIs(libtextclassifier3::StatusCode::OUT_OF_RANGE));
 }
 
 TEST_F(EmbeddingIndexTest, AddSingleEmbedding) {
-  PropertyProto::VectorProto vector = CreateVector("model", {0.1, 0.2, 0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), test_vector1_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(0);
 
   EXPECT_THAT(
       GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                /*model_signature=*/"model"),
+                                kDefaultModelSignature),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/0), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(0.1, 0.2, 0.3));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      ElementsAre(0.1, 0.2, 0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
 TEST_F(EmbeddingIndexTest, AddSingleQuantizedEmbedding) {
-  PropertyProto::VectorProto vector = CreateVector("model", {0.1, 0.2, 0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/0), vector,
-      QUANTIZATION_TYPE_QUANTIZE_8_BIT));
+      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/0), test_vector1_,
+      QUANTIZATION_TYPE_QUANTIZE_8_BIT, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(0);
 
   EmbeddingHit hit(BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/0),
                    /*location=*/0);
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(ElementsAre(hit)));
-  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
               Eq(3 + sizeof(Quantizer)));
   EXPECT_THAT(
-      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                     hit,
-                                                     /*dimension=*/3),
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+          embedding_index_.get(), hit,
+          /*dimension=*/3, kDefaultModelSignature, kDefaultSchemaName),
       IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.1, 0.2, 0.3})));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      IsEmpty());
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
 TEST_F(EmbeddingIndexTest, AddMultipleEmbeddingsInTheSameSection) {
-  PropertyProto::VectorProto vector1 = CreateVector("model", {0.1, 0.2, 0.3});
-  PropertyProto::VectorProto vector2 =
-      CreateVector("model", {-0.1, -0.2, -0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector1,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), test_vector1_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector2,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), test_vector2_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(0);
 
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/0),
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/3))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
 TEST_F(EmbeddingIndexTest, AddMultipleQuantizedEmbeddingsInTheSameSection) {
-  PropertyProto::VectorProto vector1 = CreateVector("model", {0.1, 0.2, 0.3});
-  PropertyProto::VectorProto vector2 =
-      CreateVector("model", {-0.1, -0.2, -0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/0), vector1,
-      QUANTIZATION_TYPE_QUANTIZE_8_BIT));
+      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/0), test_vector1_,
+      QUANTIZATION_TYPE_QUANTIZE_8_BIT, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/0), vector2,
-      QUANTIZATION_TYPE_QUANTIZE_8_BIT));
+      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/0), test_vector2_,
+      QUANTIZATION_TYPE_QUANTIZE_8_BIT, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(0);
 
@@ -430,69 +570,69 @@
   EmbeddingHit hit2(BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/0),
                     /*location=*/3 + sizeof(Quantizer));
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(ElementsAre(hit1, hit2)));
-  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
               Eq(2 * (3 + sizeof(Quantizer))));  // Two quantized vectors
   EXPECT_THAT(
-      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                     hit1, /*dimension=*/3),
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+          embedding_index_.get(), hit1, /*dimension=*/3, kDefaultModelSignature,
+          kDefaultSchemaName),
       IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.1, 0.2, 0.3})));
   EXPECT_THAT(
-      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                     hit2, /*dimension=*/3),
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+          embedding_index_.get(), hit2, /*dimension=*/3, kDefaultModelSignature,
+          kDefaultSchemaName),
       IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {-0.1, -0.2, -0.3})));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      IsEmpty());
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
 TEST_F(EmbeddingIndexTest, HitsWithLowerSectionIdReturnedFirst) {
-  PropertyProto::VectorProto vector1 = CreateVector("model", {0.1, 0.2, 0.3});
-  PropertyProto::VectorProto vector2 =
-      CreateVector("model", {-0.1, -0.2, -0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/5, /*document_id=*/0), vector1,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/5, /*document_id=*/0), test_vector1_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/2, /*document_id=*/0), vector2,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/2, /*document_id=*/0), test_vector2_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(0);
 
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/2, /*document_id=*/0),
                                /*location=*/3),
                   EmbeddingHit(BasicHit(/*section_id=*/5, /*document_id=*/0),
                                /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
 TEST_F(EmbeddingIndexTest, HitsWithHigherDocumentIdReturnedFirst) {
-  PropertyProto::VectorProto vector1 = CreateVector("model", {0.1, 0.2, 0.3});
-  PropertyProto::VectorProto vector2 =
-      CreateVector("model", {-0.1, -0.2, -0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector1,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), test_vector1_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/1), vector2,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/0, /*document_id=*/1), test_vector2_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(1);
 
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/1),
                                /*location=*/3),
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
 }
 
@@ -502,10 +642,10 @@
       CreateVector("model2", {-0.1, -0.2, -0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(/*section_id=*/0, /*document_id=*/0), vector1,
-      QUANTIZATION_TYPE_NONE));
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(/*section_id=*/0, /*document_id=*/0), vector2,
-      QUANTIZATION_TYPE_NONE));
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(0);
 
@@ -518,42 +658,69 @@
                                         /*model_signature=*/"model2"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
-                               /*location=*/2))));
+                               /*location=*/0))));
   EXPECT_THAT(GetEmbeddingHitsFromIndex(
                   embedding_index_.get(),
                   /*dimension=*/5, /*model_signature=*/"non-existent-model"),
               IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(0.1, 0.2, -0.1, -0.2, -0.3));
+  // Check the shard for vector1.
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(
+                  embedding_index_.get(),
+                  embedding_index_->GetShardId(/*dimension=*/2,
+                                               /*model_signature=*/"model1",
+                                               kDefaultSchemaName)),
+              ElementsAre(0.1, 0.2));
+  // Check the shard for vector2.
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(
+                  embedding_index_.get(),
+                  embedding_index_->GetShardId(/*dimension=*/3,
+                                               /*model_signature=*/"model2",
+                                               kDefaultSchemaName)),
+              ElementsAre(-0.1, -0.2, -0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
 TEST_F(EmbeddingIndexTest,
        AddEmbeddingsWithSameSignatureButDifferentDimension) {
-  PropertyProto::VectorProto vector1 = CreateVector("model", {0.1, 0.2});
+  PropertyProto::VectorProto vector1 =
+      CreateVector(kDefaultModelSignature, {0.1, 0.2});
   PropertyProto::VectorProto vector2 =
-      CreateVector("model", {-0.1, -0.2, -0.3});
+      CreateVector(kDefaultModelSignature, {-0.1, -0.2, -0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(/*section_id=*/0, /*document_id=*/0), vector1,
-      QUANTIZATION_TYPE_NONE));
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(/*section_id=*/0, /*document_id=*/0), vector2,
-      QUANTIZATION_TYPE_NONE));
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(0);
 
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/2,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/0))));
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
-                               /*location=*/2))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(0.1, 0.2, -0.1, -0.2, -0.3));
+                               /*location=*/0))));
+  // Check the shard for vector1.
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(
+          embedding_index_.get(),
+          embedding_index_->GetShardId(
+              /*dimension=*/2,
+              /*model_signature=*/kDefaultModelSignature, kDefaultSchemaName)),
+      ElementsAre(0.1, 0.2));
+  // Check the shard for vector2.
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(
+          embedding_index_.get(),
+          embedding_index_->GetShardId(
+              /*dimension=*/3,
+              /*model_signature=*/kDefaultModelSignature, kDefaultSchemaName)),
+      ElementsAre(-0.1, -0.2, -0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
@@ -561,20 +728,15 @@
   // Loop the same logic twice to make sure that clear works as expected, and
   // the index is still valid after clearing.
   for (int i = 0; i < 2; i++) {
-    PropertyProto::VectorProto vector1 = CreateVector("model", {0.1, 0.2, 0.3});
-    PropertyProto::VectorProto vector2 =
-        CreateVector("model", {-0.1, -0.2, -0.3});
-    PropertyProto::VectorProto vector3 = CreateVector("model", {0.4, 0.5, 0.6});
-
     ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-        BasicHit(/*section_id=*/1, /*document_id=*/0), vector1,
-        QUANTIZATION_TYPE_NONE));
+        BasicHit(/*section_id=*/1, /*document_id=*/0), test_vector1_,
+        QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
     ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-        BasicHit(/*section_id=*/2, /*document_id=*/1), vector2,
-        QUANTIZATION_TYPE_NONE));
+        BasicHit(/*section_id=*/2, /*document_id=*/1), test_vector2_,
+        QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
     ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-        BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2), vector3,
-        QUANTIZATION_TYPE_QUANTIZE_8_BIT));
+        BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2),
+        test_vector3_, QUANTIZATION_TYPE_QUANTIZE_8_BIT, kDefaultSchemaName));
     ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
     embedding_index_->set_last_added_document_id(2);
 
@@ -587,17 +749,20 @@
 
     EXPECT_THAT(
         GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                  /*model_signature=*/"model"),
+                                  kDefaultModelSignature),
         IsOkAndHolds(ElementsAre(hit1, hit2, hit3)));
-    EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-                ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
-    EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
-                Eq(3 + sizeof(Quantizer)));
     EXPECT_THAT(
-        GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                       hit1,
-                                                       /*dimension=*/3),
-        IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), vector3.values())));
+        GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+        ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
+    EXPECT_THAT(
+        embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
+        Eq(3 + sizeof(Quantizer)));
+    EXPECT_THAT(
+        GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+            embedding_index_.get(), hit1,
+            /*dimension=*/3, kDefaultModelSignature, kDefaultSchemaName),
+        IsOkAndHolds(
+            Pointwise(FloatNear(kEpsQuantized), test_vector3_.values())));
     EXPECT_EQ(embedding_index_->last_added_document_id(), 2);
     EXPECT_FALSE(embedding_index_->is_empty());
     EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(false));
@@ -606,9 +771,12 @@
     ICING_ASSERT_OK(embedding_index_->Clear());
     EXPECT_TRUE(embedding_index_->is_empty());
     EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(true));
-    EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-                IsEmpty());
-    EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(), Eq(0));
+    EXPECT_THAT(
+        GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+        IsEmpty());
+    EXPECT_THAT(
+        embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
+        Eq(0));
     EXPECT_EQ(embedding_index_->last_added_document_id(), kInvalidDocumentId);
   }
 }
@@ -617,20 +785,15 @@
   // Loop the same logic twice to make sure that Discard works as expected, and
   // the index is still valid after discarding.
   for (int i = 0; i < 2; i++) {
-    PropertyProto::VectorProto vector1 = CreateVector("model", {0.1, 0.2, 0.3});
-    PropertyProto::VectorProto vector2 =
-        CreateVector("model", {-0.1, -0.2, -0.3});
-    PropertyProto::VectorProto vector3 = CreateVector("model", {0.4, 0.5, 0.6});
-
     ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-        BasicHit(/*section_id=*/1, /*document_id=*/0), vector1,
-        QUANTIZATION_TYPE_NONE));
+        BasicHit(/*section_id=*/1, /*document_id=*/0), test_vector1_,
+        QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
     ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-        BasicHit(/*section_id=*/2, /*document_id=*/1), vector2,
-        QUANTIZATION_TYPE_NONE));
+        BasicHit(/*section_id=*/2, /*document_id=*/1), test_vector2_,
+        QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
     ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-        BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2), vector3,
-        QUANTIZATION_TYPE_QUANTIZE_8_BIT));
+        BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2),
+        test_vector3_, QUANTIZATION_TYPE_QUANTIZE_8_BIT, kDefaultSchemaName));
     ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
     embedding_index_->set_last_added_document_id(2);
 
@@ -642,17 +805,20 @@
                       /*location=*/0);
     EXPECT_THAT(
         GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                  /*model_signature=*/"model"),
+                                  kDefaultModelSignature),
         IsOkAndHolds(ElementsAre(hit1, hit2, hit3)));
-    EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-                ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
-    EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
-                Eq(3 + sizeof(Quantizer)));
     EXPECT_THAT(
-        GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                       hit1,
-                                                       /*dimension=*/3),
-        IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), vector3.values())));
+        GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+        ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
+    EXPECT_THAT(
+        embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
+        Eq(3 + sizeof(Quantizer)));
+    EXPECT_THAT(
+        GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+            embedding_index_.get(), hit1,
+            /*dimension=*/3, kDefaultModelSignature, kDefaultSchemaName),
+        IsOkAndHolds(
+            Pointwise(FloatNear(kEpsQuantized), test_vector3_.values())));
     EXPECT_EQ(embedding_index_->last_added_document_id(), 2);
     EXPECT_FALSE(embedding_index_->is_empty());
     EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(false));
@@ -663,12 +829,16 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         embedding_index_,
         EmbeddingIndex::Create(&filesystem_, embedding_index_dir_, &clock_,
-                               feature_flags_.get()));
+                               feature_flags_.get(),
+                               /*num_shards=*/32));
     EXPECT_TRUE(embedding_index_->is_empty());
     EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(true));
-    EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-                IsEmpty());
-    EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(), Eq(0));
+    EXPECT_THAT(
+        GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+        IsEmpty());
+    EXPECT_THAT(
+        embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
+        Eq(0));
     EXPECT_EQ(embedding_index_->last_added_document_id(), kInvalidDocumentId);
   }
 }
@@ -677,68 +847,60 @@
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   EXPECT_TRUE(embedding_index_->is_empty());
   EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(true));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
-  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(), Eq(0));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      IsEmpty());
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
+              Eq(0));
 }
 
 TEST_F(EmbeddingIndexTest, MultipleCommits) {
-  PropertyProto::VectorProto vector1 = CreateVector("model", {0.1, 0.2, 0.3});
-  PropertyProto::VectorProto vector2 =
-      CreateVector("model", {-0.1, -0.2, -0.3});
-
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/1, /*document_id=*/0), vector1,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/1, /*document_id=*/0), test_vector1_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector2,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), test_vector2_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/3),
                   EmbeddingHit(BasicHit(/*section_id=*/1, /*document_id=*/0),
                                /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
 }
 
 TEST_F(EmbeddingIndexTest,
        InvalidCommit_SectionIdCanOnlyDecreaseForSingleDocument) {
-  PropertyProto::VectorProto vector1 = CreateVector("model", {0.1, 0.2, 0.3});
-  PropertyProto::VectorProto vector2 =
-      CreateVector("model", {-0.1, -0.2, -0.3});
-
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector1,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), test_vector1_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/1, /*document_id=*/0), vector2,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/1, /*document_id=*/0), test_vector2_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   // Posting list with delta encoding can only allow decreasing values.
   EXPECT_THAT(embedding_index_->CommitBufferToIndex(),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
 TEST_F(EmbeddingIndexTest, InvalidCommit_DocumentIdCanOnlyIncrease) {
-  PropertyProto::VectorProto vector1 = CreateVector("model", {0.1, 0.2, 0.3});
-  PropertyProto::VectorProto vector2 =
-      CreateVector("model", {-0.1, -0.2, -0.3});
-
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/1), vector1,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/0, /*document_id=*/1), test_vector1_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector2,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), test_vector2_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   // Posting list with delta encoding can only allow decreasing values, which
   // means document ids must be committed increasingly, since document ids are
   // inverted in hit values.
@@ -761,10 +923,9 @@
 }
 
 TEST_F(EmbeddingIndexTest, OptimizeShouldFailWhenDocumentIdMapIsTooSmall) {
-  PropertyProto::VectorProto vector = CreateVector("model", {0.1, 0.2, 0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/2), vector,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/0, /*document_id=*/2), test_vector1_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(2);
 
@@ -785,26 +946,29 @@
       /*new_last_added_document_id=*/kInvalidDocumentId));
   EXPECT_TRUE(embedding_index_->is_empty());
   EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(true));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
-  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(), Eq(0));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      IsEmpty());
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
+              Eq(0));
 }
 
 TEST_F(EmbeddingIndexTest, OptimizeSingleEmbeddingSingleDocument) {
-  PropertyProto::VectorProto vector = CreateVector("model", {0.1, 0.2, 0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/2), vector,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/0, /*document_id=*/2), test_vector1_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(2);
 
   // Before optimize
   EXPECT_THAT(
       GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                /*model_signature=*/"model"),
+                                kDefaultModelSignature),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/2), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(0.1, 0.2, 0.3));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      ElementsAre(0.1, 0.2, 0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 2);
 
   // Run optimize without deleting any documents, and check that the index is
@@ -815,11 +979,12 @@
                                  /*new_last_added_document_id=*/2));
   EXPECT_THAT(
       GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                /*model_signature=*/"model"),
+                                kDefaultModelSignature),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/2), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(0.1, 0.2, 0.3));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      ElementsAre(0.1, 0.2, 0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 2);
 
   // Run optimize to map document id 2 to 1, and check that the index is
@@ -830,11 +995,12 @@
       /*new_last_added_document_id=*/1));
   EXPECT_THAT(
       GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                /*model_signature=*/"model"),
+                                kDefaultModelSignature),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/1), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(0.1, 0.2, 0.3));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      ElementsAre(0.1, 0.2, 0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
 
   // Run optimize to delete the document.
@@ -843,19 +1009,20 @@
       /*document_id_old_to_new=*/{0, kInvalidDocumentId},
       /*new_last_added_document_id=*/0));
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(IsEmpty()));
   EXPECT_TRUE(embedding_index_->is_empty());
   EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(true));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      IsEmpty());
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
 TEST_F(EmbeddingIndexTest, OptimizeSingleQuantizedEmbeddingSingleDocument) {
-  PropertyProto::VectorProto vector = CreateVector("model", {0.1, 0.2, 0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2), vector,
-      QUANTIZATION_TYPE_QUANTIZE_8_BIT));
+      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2), test_vector1_,
+      QUANTIZATION_TYPE_QUANTIZE_8_BIT, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(2);
 
@@ -863,15 +1030,18 @@
   EmbeddingHit hit(BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2),
                    /*location=*/0);
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(ElementsAre(hit)));
-  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
               Eq(3 + sizeof(Quantizer)));
   EXPECT_THAT(
-      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                     hit, /*dimension=*/3),
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+          embedding_index_.get(), hit, /*dimension=*/3, kDefaultModelSignature,
+          kDefaultSchemaName),
       IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.1, 0.2, 0.3})));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      IsEmpty());
   EXPECT_EQ(embedding_index_->last_added_document_id(), 2);
 
   // Run optimize without deleting any documents, and check that the index is
@@ -881,15 +1051,18 @@
                                  /*document_id_old_to_new=*/{0, 1, 2},
                                  /*new_last_added_document_id=*/2));
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(ElementsAre(hit)));
-  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
               Eq(3 + sizeof(Quantizer)));
   EXPECT_THAT(
-      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                     hit, /*dimension=*/3),
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+          embedding_index_.get(), hit, /*dimension=*/3, kDefaultModelSignature,
+          kDefaultSchemaName),
       IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.1, 0.2, 0.3})));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      IsEmpty());
   EXPECT_EQ(embedding_index_->last_added_document_id(), 2);
 
   // Run optimize to map document id 2 to 1, and check that the index is
@@ -901,15 +1074,18 @@
   hit = EmbeddingHit(BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/1),
                      /*location=*/0);
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(ElementsAre(hit)));
-  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
               Eq(3 + sizeof(Quantizer)));
   EXPECT_THAT(
-      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                     hit, /*dimension=*/3),
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+          embedding_index_.get(), hit, /*dimension=*/3, kDefaultModelSignature,
+          kDefaultSchemaName),
       IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.1, 0.2, 0.3})));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      IsEmpty());
   EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
 
   // Run optimize to delete the document
@@ -918,30 +1094,28 @@
       /*document_id_old_to_new=*/{0, kInvalidDocumentId},
       /*new_last_added_document_id=*/0));
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(IsEmpty()));
   EXPECT_TRUE(embedding_index_->is_empty());
   EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(true));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
-  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(), Eq(0));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      IsEmpty());
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
+              Eq(0));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
 TEST_F(EmbeddingIndexTest, OptimizeMultipleEmbeddingsSingleDocument) {
-  PropertyProto::VectorProto vector1 = CreateVector("model", {0.1, 0.2, 0.3});
-  PropertyProto::VectorProto vector2 =
-      CreateVector("model", {-0.1, -0.2, -0.3});
-  PropertyProto::VectorProto vector3 = CreateVector("model", {0.4, 0.5, 0.6});
-
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/2), vector1,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/0, /*document_id=*/2), test_vector1_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/2), vector2,
-      QUANTIZATION_TYPE_NONE));
+      BasicHit(/*section_id=*/0, /*document_id=*/2), test_vector2_,
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2), vector3,
-      QUANTIZATION_TYPE_QUANTIZE_8_BIT));
+      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2), test_vector3_,
+      QUANTIZATION_TYPE_QUANTIZE_8_BIT, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(2);
 
@@ -950,22 +1124,23 @@
       BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2),
       /*location=*/0);
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/2),
                                /*location=*/0),
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/2),
                                /*location=*/3),
                   quantized_hit)));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
-  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
-              Eq(3 + sizeof(Quantizer)));
   EXPECT_THAT(
-      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                     quantized_hit,
-                                                     /*dimension=*/3),
-      IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), vector3.values())));
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
+              Eq(3 + sizeof(Quantizer)));
+  EXPECT_THAT(GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+                  embedding_index_.get(), quantized_hit,
+                  /*dimension=*/3, kDefaultModelSignature, kDefaultSchemaName),
+              IsOkAndHolds(
+                  Pointwise(FloatNear(kEpsQuantized), test_vector3_.values())));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 2);
 
   // Run optimize without deleting any documents, and check that the index is
@@ -975,22 +1150,23 @@
                                  /*document_id_old_to_new=*/{0, 1, 2},
                                  /*new_last_added_document_id=*/2));
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/2),
                                /*location=*/0),
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/2),
                                /*location=*/3),
                   quantized_hit)));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
-  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
-              Eq(3 + sizeof(Quantizer)));
   EXPECT_THAT(
-      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                     quantized_hit,
-                                                     /*dimension=*/3),
-      IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), vector3.values())));
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
+              Eq(3 + sizeof(Quantizer)));
+  EXPECT_THAT(GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+                  embedding_index_.get(), quantized_hit,
+                  /*dimension=*/3, kDefaultModelSignature, kDefaultSchemaName),
+              IsOkAndHolds(
+                  Pointwise(FloatNear(kEpsQuantized), test_vector3_.values())));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 2);
 
   // Run optimize to map document id 2 to 1, and check that the index is
@@ -1003,22 +1179,23 @@
       EmbeddingHit(BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/1),
                    /*location=*/0);
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/1),
                                /*location=*/0),
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/1),
                                /*location=*/3),
                   quantized_hit)));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
-  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
-              Eq(3 + sizeof(Quantizer)));
   EXPECT_THAT(
-      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                     quantized_hit,
-                                                     /*dimension=*/3),
-      IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), vector3.values())));
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
+              Eq(3 + sizeof(Quantizer)));
+  EXPECT_THAT(GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+                  embedding_index_.get(), quantized_hit,
+                  /*dimension=*/3, kDefaultModelSignature, kDefaultSchemaName),
+              IsOkAndHolds(
+                  Pointwise(FloatNear(kEpsQuantized), test_vector3_.values())));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
 
   // Run optimize to delete the document.
@@ -1027,34 +1204,40 @@
       /*document_id_old_to_new=*/{0, kInvalidDocumentId},
       /*new_last_added_document_id=*/0));
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(IsEmpty()));
   EXPECT_TRUE(embedding_index_->is_empty());
   EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(true));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
-  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(), Eq(0));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      IsEmpty());
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
+              Eq(0));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
 TEST_F(EmbeddingIndexTest, OptimizeMultipleEmbeddingsMultipleDocument) {
-  PropertyProto::VectorProto vector1 = CreateVector("model", {0.1, 0.2, 0.3});
-  PropertyProto::VectorProto vector2 = CreateVector("model", {1, 2, 3});
+  PropertyProto::VectorProto vector1 =
+      CreateVector(kDefaultModelSignature, {0.1, 0.2, 0.3});
+  PropertyProto::VectorProto vector2 =
+      CreateVector(kDefaultModelSignature, {1, 2, 3});
   PropertyProto::VectorProto vector3 =
-      CreateVector("model", {-0.1, -0.2, -0.3});
-  PropertyProto::VectorProto vector4 = CreateVector("model", {0.4, 0.5, 0.6});
+      CreateVector(kDefaultModelSignature, {-0.1, -0.2, -0.3});
+  PropertyProto::VectorProto vector4 =
+      CreateVector(kDefaultModelSignature, {0.4, 0.5, 0.6});
 
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(/*section_id=*/0, /*document_id=*/0), vector1,
-      QUANTIZATION_TYPE_NONE));
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(/*section_id=*/1, /*document_id=*/0), vector2,
-      QUANTIZATION_TYPE_NONE));
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(/*section_id=*/0, /*document_id=*/1), vector3,
-      QUANTIZATION_TYPE_NONE));
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/1), vector4,
-      QUANTIZATION_TYPE_QUANTIZE_8_BIT));
+      QUANTIZATION_TYPE_QUANTIZE_8_BIT, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(1);
 
@@ -1063,7 +1246,7 @@
       BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/1),
       /*location=*/0);
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/1),
                                /*location=*/6),
@@ -1072,14 +1255,15 @@
                                /*location=*/0),
                   EmbeddingHit(BasicHit(/*section_id=*/1, /*document_id=*/0),
                                /*location=*/3))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(0.1, 0.2, 0.3, 1, 2, 3, -0.1, -0.2, -0.3));
-  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      ElementsAre(0.1, 0.2, 0.3, 1, 2, 3, -0.1, -0.2, -0.3));
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
               Eq(3 + sizeof(Quantizer)));
   EXPECT_THAT(
-      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                     quantized_hit,
-                                                     /*dimension=*/3),
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+          embedding_index_.get(), quantized_hit,
+          /*dimension=*/3, kDefaultModelSignature, kDefaultSchemaName),
       IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), vector4.values())));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
 
@@ -1096,7 +1280,7 @@
                                    /*new_last_added_document_id=*/1));
     EXPECT_THAT(
         GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                  /*model_signature=*/"model"),
+                                  kDefaultModelSignature),
         IsOkAndHolds(ElementsAre(
             EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/1),
                          /*location=*/0),
@@ -1105,14 +1289,16 @@
                          /*location=*/3),
             EmbeddingHit(BasicHit(/*section_id=*/1, /*document_id=*/0),
                          /*location=*/6))));
-    EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-                ElementsAre(-0.1, -0.2, -0.3, 0.1, 0.2, 0.3, 1, 2, 3));
-    EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
-                Eq(3 + sizeof(Quantizer)));
     EXPECT_THAT(
-        GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                       quantized_hit,
-                                                       /*dimension=*/3),
+        GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+        ElementsAre(-0.1, -0.2, -0.3, 0.1, 0.2, 0.3, 1, 2, 3));
+    EXPECT_THAT(
+        embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
+        Eq(3 + sizeof(Quantizer)));
+    EXPECT_THAT(
+        GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+            embedding_index_.get(), quantized_hit,
+            /*dimension=*/3, kDefaultModelSignature, kDefaultSchemaName),
         IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), vector4.values())));
     EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
   }
@@ -1127,19 +1313,20 @@
       EmbeddingHit(BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/0),
                    /*location=*/0);
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
-                                        /*model_signature=*/"model"),
+                                        kDefaultModelSignature),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/0),
                   quantized_hit)));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(-0.1, -0.2, -0.3));
-  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), default_shard_id_),
+      ElementsAre(-0.1, -0.2, -0.3));
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(default_shard_id_),
               Eq(3 + sizeof(Quantizer)));
   EXPECT_THAT(
-      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                     quantized_hit,
-                                                     /*dimension=*/3),
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+          embedding_index_.get(), quantized_hit,
+          /*dimension=*/3, kDefaultModelSignature, kDefaultSchemaName),
       IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), vector4.values())));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
@@ -1149,15 +1336,19 @@
   PropertyProto::VectorProto vector2 = CreateVector("model1", {1, 2});
   PropertyProto::VectorProto vector3 =
       CreateVector("model2", {-0.1, -0.2, -0.3});
+  uint32_t vector1_and_vector2_shard_id = embedding_index_->GetShardId(
+      /*dimension=*/2, "model1", kDefaultSchemaName);
+  uint32_t vector3_shard_id = embedding_index_->GetShardId(
+      /*dimension=*/3, "model2", kDefaultSchemaName);
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(/*section_id=*/0, /*document_id=*/0), vector1,
-      QUANTIZATION_TYPE_NONE));
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(/*section_id=*/0, /*document_id=*/1), vector2,
-      QUANTIZATION_TYPE_NONE));
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(/*section_id=*/1, /*document_id=*/1), vector3,
-      QUANTIZATION_TYPE_NONE));
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(1);
 
@@ -1173,19 +1364,17 @@
                                         /*model_signature=*/"model2"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/1, /*document_id=*/1),
-                               /*location=*/4))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(0.1, 0.2, 1, 2, -0.1, -0.2, -0.3));
+                               /*location=*/0))));
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get(),
+                                           vector1_and_vector2_shard_id),
+              ElementsAre(0.1, 0.2, 1, 2));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), vector3_shard_id),
+      ElementsAre(-0.1, -0.2, -0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
 
-  // Run optimize without deleting any documents. It is expected to see that the
-  // raw embedding data is rearranged, since during index transfer:
-  // - Embedding vectors with lower keys, which are the string encoded ordered
-  //   pairs (dimension, model_signature), are iterated first.
-  // - Embedding vectors from higher document ids are added first.
-  //
-  // Also keep in mind that once the raw data is rearranged, calling another
-  // Optimize subsequently will not change the raw data again.
+  // Run optimize without deleting any documents. We should see the same data in
+  // all the shards, since each shard only contains one vector.
   for (int i = 0; i < 2; i++) {
     ICING_ASSERT_OK(
         embedding_index_->Optimize(document_store_.get(), schema_store_.get(),
@@ -1204,9 +1393,13 @@
                                   /*model_signature=*/"model2"),
         IsOkAndHolds(ElementsAre(
             EmbeddingHit(BasicHit(/*section_id=*/1, /*document_id=*/1),
-                         /*location=*/4))));
-    EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-                ElementsAre(1, 2, 0.1, 0.2, -0.1, -0.2, -0.3));
+                         /*location=*/0))));
+    EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get(),
+                                             vector1_and_vector2_shard_id),
+                ElementsAre(1, 2, 0.1, 0.2));
+    EXPECT_THAT(
+        GetRawEmbeddingDataFromIndex(embedding_index_.get(), vector3_shard_id),
+        ElementsAre(-0.1, -0.2, -0.3));
     EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
   }
 
@@ -1224,8 +1417,12 @@
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
                                         /*model_signature=*/"model2"),
               IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get(),
+                                           vector1_and_vector2_shard_id),
               ElementsAre(0.1, 0.2));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), vector3_shard_id),
+      IsEmpty());
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
@@ -1234,12 +1431,16 @@
   PropertyProto::VectorProto vector1 = CreateVector("model1", {0.1, 0.2});
   PropertyProto::VectorProto vector2 =
       CreateVector("model2", {-0.1, -0.2, -0.3});
+  uint32_t vector1_shard_id = embedding_index_->GetShardId(
+      /*dimension=*/2, "model1", kDefaultSchemaName);
+  uint32_t vector2_shard_id = embedding_index_->GetShardId(
+      /*dimension=*/3, "model2", kDefaultSchemaName);
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(/*section_id=*/0, /*document_id=*/0), vector1,
-      QUANTIZATION_TYPE_NONE));
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(/*section_id=*/1, /*document_id=*/1), vector2,
-      QUANTIZATION_TYPE_NONE));
+      QUANTIZATION_TYPE_NONE, kDefaultSchemaName));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(1);
 
@@ -1253,9 +1454,13 @@
                                         /*model_signature=*/"model2"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/1, /*document_id=*/1),
-                               /*location=*/2))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(0.1, 0.2, -0.1, -0.2, -0.3));
+                               /*location=*/0))));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), vector1_shard_id),
+      ElementsAre(0.1, 0.2));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), vector2_shard_id),
+      ElementsAre(-0.1, -0.2, -0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
 
   // Run optimize to delete document 0, and check that the index is
@@ -1272,11 +1477,59 @@
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/1, /*document_id=*/0),
                                /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
-              ElementsAre(-0.1, -0.2, -0.3));
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), vector1_shard_id),
+      IsEmpty());
+  EXPECT_THAT(
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), vector2_shard_id),
+      ElementsAre(-0.1, -0.2, -0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
 }  // namespace
+
+TEST_F(EmbeddingIndexTest, ShardFileShouldBeLazilyCreated) {
+  // Check no shard file exists after initialization.
+  std::vector<std::string> files;
+  ASSERT_TRUE(filesystem_.ListDirectory(embedding_index_dir_.c_str(),
+                                        /*exclude=*/{},
+                                        /*recursive=*/true, &files));
+  for (const std::string& file : files) {
+    EXPECT_THAT(file, Not(HasSubstr("embedding_vectors")));
+  }
+
+  // Add 5 embeddings, each with a different schema name, and should (possibly)
+  // have a different shard id.
+  std::unordered_set<uint32_t> shard_ids;
+  for (int i = 0; i < 5; i++) {
+    std::string schema_name = "schema" + std::to_string(i);
+    uint32_t shard_id = embedding_index_->GetShardId(
+        kDefaultDimension, kDefaultModelSignature, schema_name);
+    shard_ids.insert(shard_id);
+
+    // Add a non-quantized embedding, and check if its corresponding shard
+    // file is created.
+    std::string vector_file = absl_ports::StrCat(
+        embedding_index_dir_, "/embedding_vectors_", std::to_string(shard_id));
+    EXPECT_FALSE(filesystem_.FileExists(vector_file.c_str()));
+    ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+        BasicHit(/*section_id=*/0, /*document_id=*/0), test_vector1_,
+        QUANTIZATION_TYPE_NONE, schema_name));
+    EXPECT_TRUE(filesystem_.FileExists(vector_file.c_str()));
+
+    // Add a quantized embedding, and check if its corresponding shard file
+    // is created.
+    std::string quantized_vector_file = absl_ports::StrCat(
+        embedding_index_dir_, "/quantized_embedding_vectors_",
+        std::to_string(shard_id));
+    EXPECT_FALSE(filesystem_.FileExists(quantized_vector_file.c_str()));
+    ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+        BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/0),
+        test_vector1_, QUANTIZATION_TYPE_QUANTIZE_8_BIT, schema_name));
+    EXPECT_TRUE(filesystem_.FileExists(quantized_vector_file.c_str()));
+  }
+  EXPECT_EQ(shard_ids.size(), 5);
+}
+
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/index/embed/embedding-query-results.h b/icing/index/embed/embedding-query-results.h
index a90ad10..15c597d 100644
--- a/icing/index/embed/embedding-query-results.h
+++ b/icing/index/embed/embedding-query-results.h
@@ -15,21 +15,33 @@
 #ifndef ICING_INDEX_EMBED_EMBEDDING_QUERY_RESULTS_H_
 #define ICING_INDEX_EMBED_EMBEDDING_QUERY_RESULTS_H_
 
+#include <cstdint>
 #include <memory>
+#include <optional>
 #include <unordered_map>
 #include <vector>
 
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/absl_ports/canonical_errors.h"
 #include "icing/legacy/core/icing-packed-pod.h"
 #include "icing/proto/search.pb.h"
 #include "icing/schema/section.h"
+#include "icing/scoring/advanced_scoring/double-list.h"
 #include "icing/store/document-id.h"
+#include "icing/util/embedding-util.h"
+#include "icing/util/status-macros.h"
 
 namespace icing {
 namespace lib {
 
+// Stores the matched embedding infos for a single document.
 struct EmbeddingMatchInfos {
-  // A vector of semantic scores of matched embeddings.
-  std::vector<double> scores;
+  // [score_start_index, score_end_index) is the range of score indexes in the
+  // global_scores vector. If embedding section info is enabled, the same range
+  // will be used for section infos in global_section_infos as well.
+  int32_t score_start_index = 0;
+  int32_t score_end_index = 0;
 
   struct EmbeddingMatchSectionInfo {
     // The position of the matched embedding vector in a section relative to
@@ -56,33 +68,72 @@
   static_assert(icing_is_packed_pod<EmbeddingMatchSectionInfo>::value,
                 "go/icing-ubsan");
 
-  // A vector of section infos on the matched embeddings. This will be nullptr
-  // if embedding match info is not enabled for this query.
-  //
-  // When non-null, section_infos must have a 1:1 mapping with the scores
-  // vector.
-  std::unique_ptr<std::vector<EmbeddingMatchSectionInfo>> section_infos;
-
   EmbeddingMatchInfos() = default;
-
   EmbeddingMatchInfos(const EmbeddingMatchInfos& other) = delete;
   EmbeddingMatchInfos& operator=(const EmbeddingMatchInfos& other) = delete;
 
-  // Appends a score to the scores vector.
-  void AppendScore(double score) { scores.push_back(score); }
-
-  // Appends a section info to the section_infos vector, allocating if needed.
-  void AppendSectionInfo(SectionId section_id, int position) {
-    if (!section_infos) {
-      section_infos =
-          std::make_unique<std::vector<EmbeddingMatchSectionInfo>>();
+  // Appends a score to the scores vector, which is stored in the global_scores
+  // vector. score_start_index and score_end_index will be updated accordingly.
+  //
+  // Returns:
+  //   - OK, if the score is appended successfully.
+  //   - FailedPreconditionError, if the score is not contiguous with the
+  //     existing scores.
+  libtextclassifier3::Status AppendScore(std::vector<double>& global_scores,
+                                         double score) {
+    if (score_end_index == 0) {
+      score_start_index = score_end_index = global_scores.size();
     }
-    section_infos->push_back({.position = position, .section_id = section_id});
+    if (score_end_index != global_scores.size()) {
+      return absl_ports::FailedPreconditionError(
+          "Scores for the same document should be contiguous.");
+    }
+    global_scores.push_back(score);
+    score_end_index += 1;
+    return libtextclassifier3::Status::OK;
+  }
+
+  // Appends a section info to the section info vector, which is stored in the
+  // global_section_infos vector.
+  //
+  // Returns:
+  //   - OK, if the section info is appended successfully.
+  //   - FailedPreconditionError, if the section info is not appended
+  //     immediately after AppendScore.
+  libtextclassifier3::Status AppendSectionInfo(
+      std::vector<EmbeddingMatchInfos::EmbeddingMatchSectionInfo>&
+          global_section_infos,
+      SectionId section_id, int position) {
+    if (score_end_index != global_section_infos.size() + 1) {
+      return absl_ports::FailedPreconditionError(
+          "Section infos must be appended immediately after AppendScore.");
+    }
+    global_section_infos.push_back(
+        {.position = position, .section_id = section_id});
+    return libtextclassifier3::Status::OK;
   }
 };
 
 // A class to store results generated from embedding queries.
-struct EmbeddingQueryResults {
+class EmbeddingQueryResults {
+ public:
+  // Creates an empty EmbeddingQueryResults instance.
+  EmbeddingQueryResults() : EmbeddingQueryResults(/*num_query_vectors=*/0) {}
+
+  // Creates an EmbeddingQueryResults instance with the given number of query
+  // vectors.
+  EmbeddingQueryResults(int num_query_vectors)
+      : result_infos_size_(embedding_util::kEmbeddingQueryMetricTypes.size() *
+                           num_query_vectors),
+        result_infos_(
+            std::make_unique<std::optional<EmbeddingQueryMatchInfoMap>[]>(
+                result_infos_size_)) {}
+
+  int GetNumQueryVectors() const {
+    return result_infos_size_ /
+           embedding_util::kEmbeddingQueryMetricTypes.size();
+  }
+
   // Maps from DocumentId to matched embedding infos for that document.
   // For each document, its embedding match info consists of two vectors:
   // - The scores vector, which will be used in the advanced scoring language
@@ -93,11 +144,35 @@
   using EmbeddingQueryMatchInfoMap =
       std::unordered_map<DocumentId, EmbeddingMatchInfos>;
 
-  // Maps from (query_vector_index, metric_type) to EmbeddingQueryMatchInfoMap.
-  std::unordered_map<
-      int, std::unordered_map<SearchSpecProto::EmbeddingQueryMetricType::Code,
-                              EmbeddingQueryMatchInfoMap>>
-      result_infos;
+  // A centralized vector of scores for all documents. This is used to store the
+  // scores for the "this.matchedSemanticScores(...)" function.
+  std::unique_ptr<std::vector<double>> global_scores =
+      std::make_unique<std::vector<double>>();
+
+  // A centralized vector of EmbeddingMatchSectionInfo for all documents. This
+  // is used to store the section infos for the embedding query.
+  std::unique_ptr<std::vector<EmbeddingMatchInfos::EmbeddingMatchSectionInfo>>
+      global_section_infos = std::make_unique<
+          std::vector<EmbeddingMatchInfos::EmbeddingMatchSectionInfo>>();
+
+  // Get or create the MatchedInfo map for the given query_vector_index and
+  // metric_type.
+  //
+  // Returns:
+  //   - The pointer to the EmbeddingQueryMatchInfoMap map, if the map is found
+  //     or created.
+  //   - InvalidArgumentError, if the index is out of bounds.
+  libtextclassifier3::StatusOr<EmbeddingQueryMatchInfoMap*>
+  GetOrCreateMatchInfoMap(
+      int query_vector_index,
+      SearchSpecProto::EmbeddingQueryMetricType::Code metric_type) const {
+    ICING_ASSIGN_OR_RETURN(int index,
+                           GetResultInfoIndex(query_vector_index, metric_type));
+    if (!result_infos_[index].has_value()) {
+      result_infos_[index] = EmbeddingQueryMatchInfoMap();
+    }
+    return &result_infos_[index].value();
+  }
 
   // Get the MatchedInfo map for the given query_vector_index and metric_type.
   // Returns nullptr if (query_vector_index, metric_type) does not exist in the
@@ -105,17 +180,12 @@
   const EmbeddingQueryMatchInfoMap* GetMatchInfoMap(
       int query_vector_index,
       SearchSpecProto::EmbeddingQueryMetricType::Code metric_type) const {
-    // Check if a mapping exists for the query_vector_index
-    auto outer_it = result_infos.find(query_vector_index);
-    if (outer_it == result_infos.end()) {
+    libtextclassifier3::StatusOr<int> index =
+        GetResultInfoIndex(query_vector_index, metric_type);
+    if (!index.ok() || !result_infos_[index.ValueOrDie()].has_value()) {
       return nullptr;
     }
-    // Check if a mapping exists for the metric_type
-    auto inner_it = outer_it->second.find(metric_type);
-    if (inner_it == outer_it->second.end()) {
-      return nullptr;
-    }
-    return &inner_it->second;
+    return &result_infos_[index.ValueOrDie()].value();
   }
 
   // Returns the matched infos for the given query_vector_index, metric_type,
@@ -139,19 +209,62 @@
   }
 
   // Returns the matched scores for the given query_vector_index, metric_type,
-  // and doc_id. Returns nullptr if (query_vector_index, metric_type, doc_id)
-  // does not exist in the result_scores map.
-  const std::vector<double>* GetMatchedScoresForDocument(
+  // and doc_id. Returns an empty DoubleList if (query_vector_index,
+  // metric_type, doc_id) does not exist in the result_scores map.
+  //
+  // The returned DoubleList is a non-owning view of the scores vector stored
+  // within the EmbeddingQueryResults instance. The caller must ensure the
+  // lifetime of the EmbeddingQueryResults exceeds the lifetime of the returned
+  // DoubleList.
+  DoubleList GetMatchedScoresForDocument(
       int query_vector_index,
       SearchSpecProto::EmbeddingQueryMetricType::Code metric_type,
       DocumentId doc_id) const {
     const EmbeddingMatchInfos* match_infos =
         GetMatchedInfosForDocument(query_vector_index, metric_type, doc_id);
     if (match_infos == nullptr) {
-      return nullptr;
+      return DoubleList();
     }
-    return &match_infos->scores;
+    return GetMatchedScoresFromEmbeddingMatchInfos(*match_infos);
   };
+
+  // Returns the matched scores for the given EmbeddingMatchInfos, which stores
+  // the match infos for a single document.
+  //
+  // The returned DoubleList is a non-owning view of the scores vector stored
+  // within the EmbeddingQueryResults instance. The caller must ensure the
+  // lifetime of the EmbeddingQueryResults exceeds the lifetime of the returned
+  // DoubleList.
+  DoubleList GetMatchedScoresFromEmbeddingMatchInfos(
+      const EmbeddingMatchInfos& match_infos) const {
+    return DoubleList(
+        global_scores->data() + match_infos.score_start_index,
+        match_infos.score_end_index - match_infos.score_start_index);
+  }
+
+ private:
+  // Maps from (query_vector_index, metric_type) to EmbeddingQueryMatchInfoMap.
+  int result_infos_size_;
+  std::unique_ptr<std::optional<EmbeddingQueryMatchInfoMap>[]> result_infos_;
+
+  // Returns the index of the result info for the given query_vector_index and
+  // metric_type.
+  //
+  // Returns:
+  //   - The index of the result info, if the index is valid.
+  //   - InvalidArgumentError, if the index is out of bounds.
+  libtextclassifier3::StatusOr<int> GetResultInfoIndex(
+      int query_vector_index,
+      SearchSpecProto::EmbeddingQueryMetricType::Code metric_type) const {
+    int index =
+        query_vector_index * embedding_util::kEmbeddingQueryMetricTypes.size() +
+        (metric_type - embedding_util::kEmbeddingQueryMetricTypes[0]);
+    if (result_infos_ == nullptr || index < 0 || index >= result_infos_size_) {
+      return absl_ports::InvalidArgumentError(
+          "result_infos_ index out of bounds.");
+    }
+    return index;
+  }
 };
 
 }  // namespace lib
diff --git a/icing/index/embed/embedding-scorer.cc b/icing/index/embed/embedding-scorer.cc
index 0a3ed6c..64007d4 100644
--- a/icing/index/embed/embedding-scorer.cc
+++ b/icing/index/embed/embedding-scorer.cc
@@ -26,6 +26,11 @@
 #include "icing/index/embed/quantizer.h"
 #include "icing/proto/search.pb.h"
 
+#ifndef ICING_DISABLE_EIGEN
+#include <Eigen/Core>
+#include <Eigen/Dense>
+#endif  // ICING_DISABLE_EIGEN
+
 namespace icing {
 namespace lib {
 
@@ -85,6 +90,30 @@
   return std::sqrt(result);
 }
 
+#ifndef ICING_DISABLE_EIGEN
+// Returns a lazily-evaluated (due to the return type "auto") expression that
+// dequantizes the quantized vector.
+//
+// It requires that the scale factor is not 0. If it is 0, the caller should
+// handle the case specifically.
+inline auto DequantizeEigenVector(
+    const Quantizer& quantizer,
+    Eigen::Ref<const Eigen::VectorX<uint8_t>> quantized_vec) {
+  return (quantized_vec.cast<float>().array() / quantizer.scale_factor() +
+          quantizer.float_min())
+      .matrix();
+}
+
+float EigenCosine(Eigen::Ref<const Eigen::VectorXf> v1,
+                  Eigen::Ref<const Eigen::VectorXf> v2) {
+  float divisor = v1.norm() * v2.norm();
+  if (divisor == 0.0) {
+    return 0.0;
+  }
+  return v1.dot(v2) / divisor;
+}
+#endif  // ICING_DISABLE_EIGEN
+
 }  // namespace
 
 libtextclassifier3::StatusOr<std::unique_ptr<EmbeddingScorer>>
@@ -136,5 +165,103 @@
   return CalculateEuclideanDistance(dimension, v1, v2, quantizer);
 }
 
+#ifndef ICING_DISABLE_EIGEN
+float CosineEmbeddingScorer::EigenScore(int dimension, const float* v1,
+                                        const float* v2) const {
+  Eigen::Map<const Eigen::VectorXf> vec1(v1, dimension);
+  Eigen::Map<const Eigen::VectorXf> vec2(v2, dimension);
+  return EigenCosine(vec1, vec2);
+}
+
+float DotProductEmbeddingScorer::EigenScore(int dimension, const float* v1,
+                                            const float* v2) const {
+  Eigen::Map<const Eigen::VectorXf> vec1(v1, dimension);
+  Eigen::Map<const Eigen::VectorXf> vec2(v2, dimension);
+  return vec1.dot(vec2);
+}
+
+float EuclideanDistanceEmbeddingScorer::EigenScore(int dimension,
+                                                   const float* v1,
+                                                   const float* v2) const {
+  Eigen::Map<const Eigen::VectorXf> vec1(v1, dimension);
+  Eigen::Map<const Eigen::VectorXf> vec2(v2, dimension);
+  return (vec1 - vec2).norm();
+}
+
+float CosineEmbeddingScorer::EigenScore(int dimension, const float* v1,
+                                        const uint8_t* v2,
+                                        const Quantizer& quantizer) const {
+  Eigen::Map<const Eigen::VectorXf> vec1(v1, dimension);
+  Eigen::Map<const Eigen::Matrix<uint8_t, Eigen::Dynamic, 1>> vec2(v2,
+                                                                   dimension);
+  if (quantizer.scale_factor() == 0.0) {
+    return EigenCosine(
+        vec1, Eigen::VectorXf::Constant(dimension, quantizer.float_min()));
+  }
+  return EigenCosine(vec1, DequantizeEigenVector(quantizer, vec2));
+}
+
+float DotProductEmbeddingScorer::EigenScore(int dimension, const float* v1,
+                                            const uint8_t* v2,
+                                            const Quantizer& quantizer) const {
+  Eigen::Map<const Eigen::VectorXf> vec1(v1, dimension);
+  Eigen::Map<const Eigen::Matrix<uint8_t, Eigen::Dynamic, 1>> vec2(v2,
+                                                                   dimension);
+  if (quantizer.scale_factor() == 0.0) {
+    return vec1.dot(
+        Eigen::VectorXf::Constant(dimension, quantizer.float_min()));
+  }
+  return vec1.dot(DequantizeEigenVector(quantizer, vec2));
+}
+
+float EuclideanDistanceEmbeddingScorer::EigenScore(
+    int dimension, const float* v1, const uint8_t* v2,
+    const Quantizer& quantizer) const {
+  Eigen::Map<const Eigen::VectorXf> vec1(v1, dimension);
+  Eigen::Map<const Eigen::Matrix<uint8_t, Eigen::Dynamic, 1>> vec2(v2,
+                                                                   dimension);
+  if (quantizer.scale_factor() == 0.0) {
+    return (vec1.array() - quantizer.float_min()).matrix().norm();
+  }
+  return (vec1 - DequantizeEigenVector(quantizer, vec2)).norm();
+}
+#else   // ICING_DISABLE_EIGEN
+// If Eigen is disabled, just fall back to the regular Score() function.
+
+float CosineEmbeddingScorer::EigenScore(int dimension, const float* v1,
+                                        const float* v2) const {
+  return Score(dimension, v1, v2);
+}
+
+float DotProductEmbeddingScorer::EigenScore(int dimension, const float* v1,
+                                            const float* v2) const {
+  return Score(dimension, v1, v2);
+}
+
+float EuclideanDistanceEmbeddingScorer::EigenScore(int dimension,
+                                                   const float* v1,
+                                                   const float* v2) const {
+  return Score(dimension, v1, v2);
+}
+
+float CosineEmbeddingScorer::EigenScore(int dimension, const float* v1,
+                                        const uint8_t* v2,
+                                        const Quantizer& quantizer) const {
+  return Score(dimension, v1, v2, quantizer);
+}
+
+float DotProductEmbeddingScorer::EigenScore(int dimension, const float* v1,
+                                            const uint8_t* v2,
+                                            const Quantizer& quantizer) const {
+  return Score(dimension, v1, v2, quantizer);
+}
+
+float EuclideanDistanceEmbeddingScorer::EigenScore(
+    int dimension, const float* v1, const uint8_t* v2,
+    const Quantizer& quantizer) const {
+  return Score(dimension, v1, v2, quantizer);
+}
+#endif  // ICING_DISABLE_EIGEN
+
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/index/embed/embedding-scorer.h b/icing/index/embed/embedding-scorer.h
index b0e0f75..403616a 100644
--- a/icing/index/embed/embedding-scorer.h
+++ b/icing/index/embed/embedding-scorer.h
@@ -35,6 +35,11 @@
   virtual float Score(int dimension, const float* v1, const uint8_t* v2,
                       const Quantizer& quantizer) const = 0;
 
+  virtual float EigenScore(int dimension, const float* v1,
+                           const float* v2) const = 0;
+  virtual float EigenScore(int dimension, const float* v1, const uint8_t* v2,
+                           const Quantizer& quantizer) const = 0;
+
   virtual ~EmbeddingScorer() = default;
 };
 
@@ -43,6 +48,11 @@
   float Score(int dimension, const float* v1, const float* v2) const override;
   float Score(int dimension, const float* v1, const uint8_t* v2,
               const Quantizer& quantizer) const override;
+
+  float EigenScore(int dimension, const float* v1,
+                   const float* v2) const override;
+  float EigenScore(int dimension, const float* v1, const uint8_t* v2,
+                   const Quantizer& quantizer) const override;
 };
 
 class DotProductEmbeddingScorer : public EmbeddingScorer {
@@ -50,6 +60,11 @@
   float Score(int dimension, const float* v1, const float* v2) const override;
   float Score(int dimension, const float* v1, const uint8_t* v2,
               const Quantizer& quantizer) const override;
+
+  float EigenScore(int dimension, const float* v1,
+                   const float* v2) const override;
+  float EigenScore(int dimension, const float* v1, const uint8_t* v2,
+                   const Quantizer& quantizer) const override;
 };
 
 class EuclideanDistanceEmbeddingScorer : public EmbeddingScorer {
@@ -57,6 +72,11 @@
   float Score(int dimension, const float* v1, const float* v2) const override;
   float Score(int dimension, const float* v1, const uint8_t* v2,
               const Quantizer& quantizer) const override;
+
+  float EigenScore(int dimension, const float* v1,
+                   const float* v2) const override;
+  float EigenScore(int dimension, const float* v1, const uint8_t* v2,
+                   const Quantizer& quantizer) const override;
 };
 
 }  // namespace lib
diff --git a/icing/index/embed/embedding-scorer_test.cc b/icing/index/embed/embedding-scorer_test.cc
index 162b969..035526d 100644
--- a/icing/index/embed/embedding-scorer_test.cc
+++ b/icing/index/embed/embedding-scorer_test.cc
@@ -14,8 +14,11 @@
 
 #include "icing/index/embed/embedding-scorer.h"
 
+#include <algorithm>
 #include <cstdint>
 #include <memory>
+#include <random>
+#include <tuple>
 #include <vector>
 
 #include "gtest/gtest.h"
@@ -120,6 +123,120 @@
               expected_euclidean, eps_quantized);
 }
 
+class EmbeddingScorerEigenTest
+    : public testing::TestWithParam<
+          std::tuple<SearchSpecProto::EmbeddingQueryMetricType::Code, int>> {
+ protected:
+  void SetUp() override {
+    metric_ = std::get<0>(GetParam());
+    dimension_ = std::get<1>(GetParam());
+    ICING_ASSERT_OK_AND_ASSIGN(embedding_scorer_,
+                               EmbeddingScorer::Create(metric_));
+
+    // Initialize random number generator
+    random_ = std::default_random_engine(std::random_device()());
+    dist_ = std::uniform_real_distribution<float>(-3.0f, 3.0f);
+  }
+
+  // Generates a random vector of the specified dimension.
+  std::vector<float> GenerateRandomVector() {
+    std::vector<float> vec(dimension_);
+    for (int i = 0; i < dimension_; ++i) {
+      vec[i] = dist_(random_);
+    }
+    return vec;
+  }
+
+  std::vector<float> GenerateRandomConstantVector() {
+    float value = dist_(random_);
+    std::vector<float> vec(dimension_, value);
+    return vec;
+  }
+
+  const int kNumRandomPairs = 1000;
+  const float kEps = 0.001f;
+
+  SearchSpecProto::EmbeddingQueryMetricType::Code metric_;
+  int dimension_;
+  std::unique_ptr<EmbeddingScorer> embedding_scorer_;
+  std::default_random_engine random_;
+  std::uniform_real_distribution<float> dist_;
+};
+
+// Test that the EigenScore function matches the Score function for a variety
+// of random vectors.
+TEST_P(EmbeddingScorerEigenTest, EigenScoreMatchesScore) {
+  for (int i = 0; i < kNumRandomPairs; ++i) {
+    std::vector<float> v1 = GenerateRandomVector();
+    std::vector<float> v2 = GenerateRandomVector();
+
+    // Compare scores
+    float score_val =
+        embedding_scorer_->Score(dimension_, v1.data(), v2.data());
+    float eigen_score_val =
+        embedding_scorer_->EigenScore(dimension_, v1.data(), v2.data());
+    ASSERT_NEAR(score_val, eigen_score_val, kEps);
+  }
+}
+
+// Test that the EigenScore function matches the Score function for a variety
+// of random quantized vectors.
+TEST_P(EmbeddingScorerEigenTest, EigenScoreMatchesScoreForQuantizedVectors) {
+  for (int i = 0; i < kNumRandomPairs; ++i) {
+    std::vector<float> v1 = GenerateRandomVector();
+    std::vector<float> v2 = GenerateRandomVector();
+
+    // Quantize v2
+    auto v2_minmax_pair = std::minmax_element(v2.begin(), v2.end());
+    ICING_ASSERT_OK_AND_ASSIGN(
+        Quantizer quantizer,
+        Quantizer::Create(*v2_minmax_pair.first, *v2_minmax_pair.second));
+    std::vector<uint8_t> v2_quantized = QuantizeVector(v2, quantizer);
+
+    // Compare scores
+    float score_val = embedding_scorer_->Score(dimension_, v1.data(),
+                                               v2_quantized.data(), quantizer);
+    float eigen_score_val = embedding_scorer_->EigenScore(
+        dimension_, v1.data(), v2_quantized.data(), quantizer);
+    ASSERT_NEAR(score_val, eigen_score_val, kEps);
+  }
+}
+
+// Test that the EigenScore function matches the Score function for constant
+// vectors (i.e. all values are the same) to be quantized.
+TEST_P(EmbeddingScorerEigenTest,
+       EigenScoreMatchesScoreForQuantizedConstantVectors) {
+  for (int i = 0; i < kNumRandomPairs; ++i) {
+    std::vector<float> v1 = GenerateRandomVector();
+    std::vector<float> v2 = GenerateRandomConstantVector();
+
+    // Check that v2 is constant.
+    auto v2_minmax_pair = std::minmax_element(v2.begin(), v2.end());
+    ASSERT_TRUE(*v2_minmax_pair.first == *v2_minmax_pair.second);
+    // Quantize v2
+    ICING_ASSERT_OK_AND_ASSIGN(
+        Quantizer quantizer,
+        Quantizer::Create(*v2_minmax_pair.first, *v2_minmax_pair.second));
+    ASSERT_EQ(quantizer.scale_factor(), 0.f);
+    std::vector<uint8_t> v2_quantized = QuantizeVector(v2, quantizer);
+
+    // Compare scores
+    float score_val = embedding_scorer_->Score(dimension_, v1.data(),
+                                               v2_quantized.data(), quantizer);
+    float eigen_score_val = embedding_scorer_->EigenScore(
+        dimension_, v1.data(), v2_quantized.data(), quantizer);
+    ASSERT_NEAR(score_val, eigen_score_val, kEps);
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    EigenVsScoreComparison, EmbeddingScorerEigenTest,
+    testing::Combine(
+        testing::Values(SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT,
+                        SearchSpecProto::EmbeddingQueryMetricType::COSINE,
+                        SearchSpecProto::EmbeddingQueryMetricType::EUCLIDEAN),
+        testing::Values(128, 512, 768, 1024)));
+
 }  // namespace
 
 }  // namespace lib
diff --git a/icing/index/embed/quantizer.h b/icing/index/embed/quantizer.h
index 099c96d..7095263 100644
--- a/icing/index/embed/quantizer.h
+++ b/icing/index/embed/quantizer.h
@@ -74,6 +74,9 @@
     return (quantized / scale_factor_) + float_min_;
   }
 
+  float float_min() const { return float_min_; }
+  float scale_factor() const { return scale_factor_; }
+
  private:
   static constexpr uint8_t kMaxQuantizedValue =
       std::numeric_limits<uint8_t>::max();
diff --git a/icing/index/embedding-indexing-handler.cc b/icing/index/embedding-indexing-handler.cc
index 28d6568..ec95534 100644
--- a/icing/index/embedding-indexing-handler.cc
+++ b/icing/index/embedding-indexing-handler.cc
@@ -72,7 +72,9 @@
       BasicHit hit(/*section_id=*/vector_section.metadata.id, document_id);
       for (const PropertyProto::VectorProto& vector : vector_section.content) {
         ICING_RETURN_IF_ERROR(embedding_index_.BufferEmbedding(
-            hit, vector, vector_section.metadata.quantization_type));
+            hit, vector, vector_section.metadata.quantization_type,
+            /*schema_name=*/
+            tokenized_document.document_wrapper().document().schema()));
       }
     }
     ICING_RETURN_IF_ERROR(embedding_index_.CommitBufferToIndex());
diff --git a/icing/index/embedding-indexing-handler_test.cc b/icing/index/embedding-indexing-handler_test.cc
index f2352a5..3435951 100644
--- a/icing/index/embedding-indexing-handler_test.cc
+++ b/icing/index/embedding-indexing-handler_test.cc
@@ -14,6 +14,7 @@
 
 #include "icing/index/embedding-indexing-handler.h"
 
+#include <cstdint>
 #include <initializer_list>
 #include <memory>
 #include <string>
@@ -31,6 +32,7 @@
 #include "icing/index/embed/embedding-index.h"
 #include "icing/index/embed/quantizer.h"
 #include "icing/index/hit/hit.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document_wrapper.pb.h"
 #include "icing/proto/schema.pb.h"
@@ -198,20 +200,25 @@
         filesystem_.CreateDirectoryRecursively(document_store_dir_.c_str()));
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult doc_store_create_result,
-        DocumentStore::Create(&filesystem_, document_store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, document_store_dir_, &fake_clock_,
+            schema_store_.get(), feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(doc_store_create_result.document_store);
 
     ICING_ASSERT_OK_AND_ASSIGN(
         embedding_index_,
         EmbeddingIndex::Create(&filesystem_, embedding_index_working_path_,
-                               &fake_clock_, feature_flags_.get()));
+                               &fake_clock_, feature_flags_.get(),
+                               /*num_shards=*/32));
   }
 
   void TearDown() override {
@@ -269,13 +276,18 @@
           .AddVectorProperty(std::string(kPropertyNonIndexableEmbedding),
                              CreateVector("model", {1.1, 1.2, 1.3}))
           .Build();
+  uint32_t shard_id = embedding_index_->GetShardId(
+      /*dimension=*/3, /*model_signature=*/"model",
+      /*schema_name=*/kFakeType);
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(tokenized_document.document()));
+      document_store_->Put(tokenized_document.document_wrapper()));
   DocumentId document_id = put_result.new_document_id;
 
   ASSERT_THAT(embedding_index_->last_added_document_id(),
@@ -310,20 +322,22 @@
               IsOkAndHolds(ElementsAre(hit1, hit2, quantized_hit1,
                                        quantized_hit2, hit3)));
   // Check unquantized embedding data
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get(), shard_id),
               ElementsAre(0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1, 0.2, 0.3));
   // Check quantized embedding data
-  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(shard_id),
               Eq(6 + 2 * sizeof(Quantizer)));
   EXPECT_THAT(
-      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                     quantized_hit1,
-                                                     /*dimension=*/3),
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+          embedding_index_.get(), quantized_hit1,
+          /*dimension=*/3, /*model_signature=*/"model",
+          /*schema_name=*/kFakeType),
       IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.1, 0.2, 0.3})));
   EXPECT_THAT(
-      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                     quantized_hit2,
-                                                     /*dimension=*/3),
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+          embedding_index_.get(), quantized_hit2,
+          /*dimension=*/3, /*model_signature=*/"model",
+          /*schema_name=*/kFakeType),
       IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.4, 0.5, 0.6})));
 
   EXPECT_THAT(embedding_index_->last_added_document_id(), Eq(document_id));
@@ -344,13 +358,18 @@
           .AddVectorProperty(std::string(kPropertyNonIndexableEmbedding),
                              CreateVector("model", {1.1, 1.2, 1.3}))
           .Build();
+  uint32_t shard_id = embedding_index_->GetShardId(
+      /*dimension=*/3, /*model_signature=*/"model",
+      /*schema_name=*/kFakeType);
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(tokenized_document.document()));
+      document_store_->Put(tokenized_document.document_wrapper()));
   DocumentId document_id = put_result.new_document_id;
 
   ASSERT_THAT(embedding_index_->last_added_document_id(),
@@ -370,7 +389,8 @@
   EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
                                         /*model_signature=*/"model"),
               IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get(), shard_id),
+              IsEmpty());
   EXPECT_THAT(embedding_index_->last_added_document_id(), Eq(document_id));
 }
 
@@ -401,13 +421,18 @@
           .AddVectorProperty(std::string(kPropertyFullDocEmbedding),
                              CreateVector("model", {2.1, 2.2, 2.3}))
           .Build();
+  uint32_t shard_id = embedding_index_->GetShardId(
+      /*dimension=*/3, /*model_signature=*/"model",
+      /*schema_name=*/kFakeCollectionType);
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(tokenized_document.document()));
+      document_store_->Put(tokenized_document.document_wrapper()));
   DocumentId document_id = put_result.new_document_id;
 
   ASSERT_THAT(embedding_index_->last_added_document_id(),
@@ -445,20 +470,22 @@
                                        quantized_hit2, hit3, hit4)));
   // Check unquantized embedding data
   EXPECT_THAT(
-      GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+      GetRawEmbeddingDataFromIndex(embedding_index_.get(), shard_id),
       ElementsAre(0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1, 0.2, 0.3, 2.1, 2.2, 2.3));
   // Check quantized embedding data
-  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(shard_id),
               Eq(6 + 2 * sizeof(Quantizer)));
   EXPECT_THAT(
-      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                     quantized_hit1,
-                                                     /*dimension=*/3),
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+          embedding_index_.get(), quantized_hit1,
+          /*dimension=*/3, /*model_signature=*/"model",
+          /*schema_name=*/kFakeCollectionType),
       IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.1, 0.2, 0.3})));
   EXPECT_THAT(
-      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
-                                                     quantized_hit2,
-                                                     /*dimension=*/3),
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+          embedding_index_.get(), quantized_hit2,
+          /*dimension=*/3, /*model_signature=*/"model",
+          /*schema_name=*/kFakeCollectionType),
       IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.4, 0.5, 0.6})));
 
   EXPECT_THAT(embedding_index_->last_added_document_id(), Eq(document_id));
@@ -480,11 +507,16 @@
           .AddVectorProperty(std::string(kPropertyNonIndexableEmbedding),
                              CreateVector("model", {1.1, 1.2, 1.3}))
           .Build();
+  uint32_t shard_id = embedding_index_->GetShardId(
+      /*dimension=*/3, /*model_signature=*/"model",
+      /*schema_name=*/kFakeType);
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
-  ICING_ASSERT_OK(document_store_->Put(tokenized_document.document()));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
+  ICING_ASSERT_OK(document_store_->Put(tokenized_document.document_wrapper()));
 
   static constexpr DocumentId kCurrentDocumentId = 3;
   embedding_index_->set_last_added_document_id(kCurrentDocumentId);
@@ -510,7 +542,8 @@
                                         /*model_signature=*/"model"),
               IsOkAndHolds(IsEmpty()));
   EXPECT_TRUE(embedding_index_->is_empty());
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get(), shard_id),
+              IsEmpty());
 
   // Recovery mode should get the same result.
   EXPECT_THAT(
@@ -525,7 +558,8 @@
                                         /*model_signature=*/"model"),
               IsOkAndHolds(IsEmpty()));
   EXPECT_TRUE(embedding_index_->is_empty());
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get(), shard_id),
+              IsEmpty());
 }
 
 TEST_F(EmbeddingIndexingHandlerTest,
@@ -544,13 +578,18 @@
           .AddVectorProperty(std::string(kPropertyNonIndexableEmbedding),
                              CreateVector("model", {1.1, 1.2, 1.3}))
           .Build();
+  uint32_t shard_id = embedding_index_->GetShardId(
+      /*dimension=*/3, /*model_signature=*/"model",
+      /*schema_name=*/kFakeType);
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(tokenized_document.document()));
+      document_store_->Put(tokenized_document.document_wrapper()));
   DocumentId document_id = put_result.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -574,7 +613,8 @@
                                         /*model_signature=*/"model"),
               IsOkAndHolds(IsEmpty()));
   EXPECT_TRUE(embedding_index_->is_empty());
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get(), shard_id),
+              IsEmpty());
 
   // Handling document with document_id < last_added_document_id should cause a
   // failure, and both index data and last_added_document_id should remain
@@ -592,7 +632,8 @@
                                         /*model_signature=*/"model"),
               IsOkAndHolds(IsEmpty()));
   EXPECT_TRUE(embedding_index_->is_empty());
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get(), shard_id),
+              IsEmpty());
 }
 
 TEST_F(EmbeddingIndexingHandlerTest,
@@ -625,21 +666,28 @@
           .AddVectorProperty(std::string(kPropertyNonIndexableEmbedding),
                              CreateVector("model", {11.1, 11.2, 11.3}))
           .Build();
+  uint32_t shard_id = embedding_index_->GetShardId(
+      /*dimension=*/3, /*model_signature=*/"model",
+      /*schema_name=*/kFakeType);
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document1,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document1)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document1)));
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document2,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document2)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document2)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(tokenized_document1.document()));
+      document_store_->Put(tokenized_document1.document_wrapper()));
   DocumentId document_id1 = put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(tokenized_document2.document()));
+      document_store_->Put(tokenized_document2.document_wrapper()));
   DocumentId document_id2 = put_result2.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -667,7 +715,7 @@
                        /*location=*/3),
           EmbeddingHit(BasicHit(kSectionIdTitleEmbedding, /*document_id=*/0),
                        /*location=*/6))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get(), shard_id),
               ElementsAre(0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1, 0.2, 0.3));
 
   // Handle document with document_id == last_added_document_id in recovery
@@ -694,7 +742,7 @@
                        /*location=*/3),
           EmbeddingHit(BasicHit(kSectionIdTitleEmbedding, /*document_id=*/0),
                        /*location=*/6))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get(), shard_id),
               ElementsAre(0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1, 0.2, 0.3));
 
   // Handle document with document_id < last_added_document_id in recovery mode.
@@ -720,7 +768,7 @@
                        /*location=*/3),
           EmbeddingHit(BasicHit(kSectionIdTitleEmbedding, /*document_id=*/0),
                        /*location=*/6))));
-  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get(), shard_id),
               ElementsAre(0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1, 0.2, 0.3));
 }
 
diff --git a/icing/index/index-processor_benchmark.cc b/icing/index/index-processor_benchmark.cc
index 7adaac4..ff41adc 100644
--- a/icing/index/index-processor_benchmark.cc
+++ b/icing/index/index-processor_benchmark.cc
@@ -158,11 +158,13 @@
 
 std::unique_ptr<Index> CreateIndex(const IcingFilesystem& icing_filesystem,
                                    const Filesystem& filesystem,
-                                   const std::string& index_dir) {
+                                   const std::string& index_dir,
+                                   const FeatureFlags& feature_flags) {
   Index::Options options(index_dir, /*index_merge_size=*/1024 * 1024 * 10,
                          /*lite_index_sort_at_indexing=*/true,
                          /*lite_index_sort_size=*/1024 * 8);
-  return Index::Create(options, &filesystem, &icing_filesystem).ValueOrDie();
+  return Index::Create(options, &filesystem, &icing_filesystem, &feature_flags)
+      .ValueOrDie();
 }
 
 std::unique_ptr<Normalizer> CreateNormalizer() {
@@ -235,7 +237,7 @@
               IsTrue());
 
   std::unique_ptr<Index> index =
-      CreateIndex(icing_filesystem, filesystem, index_dir);
+      CreateIndex(icing_filesystem, filesystem, index_dir, feature_flags);
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<NumericIndex<int64_t>> integer_index,
       IntegerIndex::Create(filesystem, integer_index_dir,
@@ -258,8 +260,9 @@
 
   DocumentProto input_document = CreateDocumentWithOneProperty(state.range(0));
   TokenizedDocument tokenized_document(std::move(
-      TokenizedDocument::Create(schema_store.get(), language_segmenter.get(),
-                                input_document)
+      TokenizedDocument::Create(
+          schema_store.get(), language_segmenter.get(),
+          /*current_time_ms=*/clock.GetSystemTimeMilliseconds(), input_document)
           .ValueOrDie()));
 
   DocumentId old_document_id = kInvalidDocumentId;
@@ -314,7 +317,7 @@
               IsTrue());
 
   std::unique_ptr<Index> index =
-      CreateIndex(icing_filesystem, filesystem, index_dir);
+      CreateIndex(icing_filesystem, filesystem, index_dir, feature_flags);
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<NumericIndex<int64_t>> integer_index,
       IntegerIndex::Create(filesystem, integer_index_dir,
@@ -338,8 +341,9 @@
   DocumentProto input_document =
       CreateDocumentWithTenProperties(state.range(0));
   TokenizedDocument tokenized_document(std::move(
-      TokenizedDocument::Create(schema_store.get(), language_segmenter.get(),
-                                input_document)
+      TokenizedDocument::Create(
+          schema_store.get(), language_segmenter.get(),
+          /*current_time_ms=*/clock.GetSystemTimeMilliseconds(), input_document)
           .ValueOrDie()));
 
   DocumentId old_document_id = kInvalidDocumentId;
@@ -394,7 +398,7 @@
               IsTrue());
 
   std::unique_ptr<Index> index =
-      CreateIndex(icing_filesystem, filesystem, index_dir);
+      CreateIndex(icing_filesystem, filesystem, index_dir, feature_flags);
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<NumericIndex<int64_t>> integer_index,
       IntegerIndex::Create(filesystem, integer_index_dir,
@@ -418,8 +422,9 @@
   DocumentProto input_document =
       CreateDocumentWithDiacriticLetters(state.range(0));
   TokenizedDocument tokenized_document(std::move(
-      TokenizedDocument::Create(schema_store.get(), language_segmenter.get(),
-                                input_document)
+      TokenizedDocument::Create(
+          schema_store.get(), language_segmenter.get(),
+          /*current_time_ms=*/clock.GetSystemTimeMilliseconds(), input_document)
           .ValueOrDie()));
 
   DocumentId old_document_id = kInvalidDocumentId;
@@ -474,7 +479,7 @@
               IsTrue());
 
   std::unique_ptr<Index> index =
-      CreateIndex(icing_filesystem, filesystem, index_dir);
+      CreateIndex(icing_filesystem, filesystem, index_dir, feature_flags);
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<NumericIndex<int64_t>> integer_index,
       IntegerIndex::Create(filesystem, integer_index_dir,
@@ -497,8 +502,9 @@
 
   DocumentProto input_document = CreateDocumentWithHiragana(state.range(0));
   TokenizedDocument tokenized_document(std::move(
-      TokenizedDocument::Create(schema_store.get(), language_segmenter.get(),
-                                input_document)
+      TokenizedDocument::Create(
+          schema_store.get(), language_segmenter.get(),
+          /*current_time_ms=*/clock.GetSystemTimeMilliseconds(), input_document)
           .ValueOrDie()));
 
   DocumentId old_document_id = kInvalidDocumentId;
diff --git a/icing/index/index-processor_test.cc b/icing/index/index-processor_test.cc
index cba75f6..2b2f4e7 100644
--- a/icing/index/index-processor_test.cc
+++ b/icing/index/index-processor_test.cc
@@ -48,6 +48,7 @@
 #include "icing/join/qualified-id-join-indexing-handler.h"
 #include "icing/legacy/index/icing-filesystem.h"
 #include "icing/legacy/index/icing-mock-filesystem.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
@@ -179,7 +180,8 @@
                            /*lite_index_sort_at_indexing=*/true,
                            /*lite_index_sort_size=*/1024 * 8);
     ICING_ASSERT_OK_AND_ASSIGN(
-        index_, Index::Create(options, &filesystem_, &icing_filesystem_));
+        index_, Index::Create(options, &filesystem_, &icing_filesystem_,
+                              feature_flags_.get()));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         integer_index_,
@@ -288,14 +290,18 @@
     ASSERT_TRUE(filesystem_.CreateDirectoryRecursively(doc_store_dir_.c_str()));
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, doc_store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, doc_store_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     doc_store_ = std::move(create_result.document_store);
 
     ICING_ASSERT_OK_AND_ASSIGN(
@@ -311,7 +317,8 @@
         std::unique_ptr<QualifiedIdJoinIndexingHandler>
             qualified_id_join_indexing_handler,
         QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                               qualified_id_join_index_.get()));
+                                               qualified_id_join_index_.get(),
+                                               feature_flags_.get()));
     std::vector<std::unique_ptr<DataIndexingHandler>> handlers;
     handlers.push_back(std::move(term_indexing_handler));
     handlers.push_back(std::move(integer_section_indexing_handler));
@@ -393,8 +400,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId0,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -411,8 +420,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId0,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -429,8 +440,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId0,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -466,8 +479,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId0,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -489,8 +504,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId1,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -551,8 +568,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId0,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -580,8 +599,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId0,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -618,8 +639,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId0,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -642,8 +665,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId0,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -680,8 +705,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId0,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -727,8 +754,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(8));
 
   EXPECT_THAT(
@@ -773,8 +802,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId0,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -789,8 +820,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId1,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -817,8 +850,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId0,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -833,8 +868,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId1,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -864,8 +901,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId1,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -888,8 +927,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId0,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -928,7 +969,8 @@
       std::unique_ptr<QualifiedIdJoinIndexingHandler>
           qualified_id_join_indexing_handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
   std::vector<std::unique_ptr<DataIndexingHandler>> handlers;
   handlers.push_back(std::move(term_indexing_handler));
   handlers.push_back(std::move(integer_section_indexing_handler));
@@ -946,8 +988,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor.IndexDocument(tokenized_document, kDocumentId1,
                                     /*old_document_id=*/kInvalidDocumentId),
@@ -971,8 +1015,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor.IndexDocument(tokenized_document, kDocumentId0,
                                     /*old_document_id=*/kInvalidDocumentId),
@@ -1013,8 +1059,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId0,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -1049,8 +1097,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document_one));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document_one));
   EXPECT_THAT(
       index_processor_->IndexDocument(tokenized_document, kDocumentId0,
                                       /*old_document_id=*/kInvalidDocumentId),
@@ -1072,14 +1122,17 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   Index::Options options(index_dir_,
                          /*index_merge_size=*/document.ByteSizeLong() * 100,
                          /*lite_index_sort_at_indexing=*/true,
                          /*lite_index_sort_size=*/64);
   ICING_ASSERT_OK_AND_ASSIGN(
-      index_, Index::Create(options, &filesystem_, &icing_filesystem_));
+      index_, Index::Create(options, &filesystem_, &icing_filesystem_,
+                            feature_flags_.get()));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<TermIndexingHandler> term_indexing_handler,
@@ -1140,8 +1193,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
 
   // 2. Recreate the index with the mock filesystem and a merge size that will
   // only allow one document to be added before requiring a merge.
@@ -1150,8 +1205,8 @@
                          /*lite_index_sort_at_indexing=*/true,
                          /*lite_index_sort_size=*/16);
   ICING_ASSERT_OK_AND_ASSIGN(
-      index_,
-      Index::Create(options, &filesystem_, mock_icing_filesystem_.get()));
+      index_, Index::Create(options, &filesystem_, mock_icing_filesystem_.get(),
+                            feature_flags_.get()));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<TermIndexingHandler> term_indexing_handler,
@@ -1198,8 +1253,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(1));
 
   EXPECT_THAT(
@@ -1232,8 +1289,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(1));
 
   EXPECT_THAT(
@@ -1268,8 +1327,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(1));
 
   EXPECT_THAT(
@@ -1301,8 +1362,10 @@
                                .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(7));
 
   EXPECT_THAT(
@@ -1352,8 +1415,10 @@
                                .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(7));
 
   EXPECT_THAT(
@@ -1384,8 +1449,10 @@
                                .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(7));
 
   EXPECT_THAT(
@@ -1437,8 +1504,10 @@
                                .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(7));
 
   EXPECT_THAT(
@@ -1470,8 +1539,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(7));
 
   EXPECT_THAT(
@@ -1530,8 +1601,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(8));
 
   EXPECT_THAT(
@@ -1574,8 +1647,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(7));
 
   EXPECT_THAT(
@@ -1628,8 +1703,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(8));
 
   EXPECT_THAT(
@@ -1682,8 +1759,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   // Expected to have 1 integer section.
   EXPECT_THAT(tokenized_document.integer_sections(), SizeIs(1));
 
@@ -1714,8 +1793,10 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          document));
   // Expected to have 1 integer section.
   EXPECT_THAT(tokenized_document.integer_sections(), SizeIs(1));
 
diff --git a/icing/index/index.cc b/icing/index/index.cc
index a03f07a..2da5cc8 100644
--- a/icing/index/index.cc
+++ b/icing/index/index.cc
@@ -27,6 +27,7 @@
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/absl_ports/str_cat.h"
+#include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
 #include "icing/index/hit/hit.h"
 #include "icing/index/iterator/doc-hit-info-iterator-or.h"
@@ -148,9 +149,11 @@
 
 libtextclassifier3::StatusOr<std::unique_ptr<Index>> Index::Create(
     const Options& options, const Filesystem* filesystem,
-    const IcingFilesystem* icing_filesystem) {
+    const IcingFilesystem* icing_filesystem,
+    const FeatureFlags* feature_flags) {
   ICING_RETURN_ERROR_IF_NULL(filesystem);
   ICING_RETURN_ERROR_IF_NULL(icing_filesystem);
+  ICING_RETURN_ERROR_IF_NULL(feature_flags);
 
   ICING_ASSIGN_OR_RETURN(LiteIndex::Options lite_index_options,
                          CreateLiteIndexOptions(options));
@@ -175,9 +178,9 @@
       std::unique_ptr<MainIndex> main_index,
       MainIndex::Create(MakeMainIndexFilepath(options.base_dir), filesystem,
                         icing_filesystem));
-  return std::unique_ptr<Index>(new Index(options, std::move(term_id_codec),
-                                          std::move(lite_index),
-                                          std::move(main_index), filesystem));
+  return std::unique_ptr<Index>(
+      new Index(options, std::move(term_id_codec), std::move(lite_index),
+                std::move(main_index), filesystem, feature_flags));
 }
 
 /* static */ libtextclassifier3::StatusOr<int> Index::ReadFlashIndexMagic(
diff --git a/icing/index/index.h b/icing/index/index.h
index fe3415c..774815c 100644
--- a/icing/index/index.h
+++ b/icing/index/index.h
@@ -25,6 +25,7 @@
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
 #include "icing/index/hit/hit.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
@@ -95,7 +96,8 @@
   //   INTERNAL on I/O error
   static libtextclassifier3::StatusOr<std::unique_ptr<Index>> Create(
       const Options& options, const Filesystem* filesystem,
-      const IcingFilesystem* icing_filesystem);
+      const IcingFilesystem* icing_filesystem,
+      const FeatureFlags* feature_flags);
 
   // Reads magic from existing flash (main) index file header. We need this
   // during Icing initialization phase to determine the version.
@@ -312,7 +314,9 @@
     ICING_RETURN_IF_ERROR(main_index_->AddHits(
         *term_id_codec_, std::move(outputs.backfill_map),
         std::move(term_id_hit_pairs), lite_index_->last_added_document_id()));
-    ICING_RETURN_IF_ERROR(main_index_->PersistToDisk());
+    if (!feature_flags_.enable_optimize_improvements()) {
+      ICING_RETURN_IF_ERROR(main_index_->PersistToDisk());
+    }
     return lite_index_->Reset();
   }
 
@@ -340,14 +344,18 @@
       DocumentId new_last_added_document_id);
 
  private:
-  Index(const Options& options, std::unique_ptr<TermIdCodec> term_id_codec,
-        std::unique_ptr<LiteIndex> lite_index,
-        std::unique_ptr<MainIndex> main_index, const Filesystem* filesystem)
+  explicit Index(const Options& options,
+                 std::unique_ptr<TermIdCodec> term_id_codec,
+                 std::unique_ptr<LiteIndex> lite_index,
+                 std::unique_ptr<MainIndex> main_index,
+                 const Filesystem* filesystem,
+                 const FeatureFlags* feature_flags)
       : lite_index_(std::move(lite_index)),
         main_index_(std::move(main_index)),
         options_(options),
         term_id_codec_(std::move(term_id_codec)),
-        filesystem_(filesystem) {}
+        filesystem_(filesystem),
+        feature_flags_(*feature_flags) {}
 
   libtextclassifier3::StatusOr<std::vector<TermMetadata>> FindLiteTermsByPrefix(
       const std::string& prefix,
@@ -358,7 +366,8 @@
   std::unique_ptr<MainIndex> main_index_;
   const Options options_;
   std::unique_ptr<TermIdCodec> term_id_codec_;
-  const Filesystem* filesystem_;
+  const Filesystem* filesystem_;       // Does not own.
+  const FeatureFlags& feature_flags_;  // Does not own.
 };
 
 }  // namespace lib
diff --git a/icing/index/index_test.cc b/icing/index/index_test.cc
index 7f73590..4925f38 100644
--- a/icing/index/index_test.cc
+++ b/icing/index/index_test.cc
@@ -30,6 +30,7 @@
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
 #include "icing/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
@@ -45,9 +46,11 @@
 #include "icing/testing/always-true-suggestion-result-checker-impl.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/random-string.h"
+#include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 #include "icing/util/crc32.h"
 #include "icing/util/logging.h"
+#include "icing/util/status-macros.h"
 
 namespace icing {
 namespace lib {
@@ -77,12 +80,15 @@
 class IndexTest : public Test {
  protected:
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+
     index_dir_ = GetTestTempDir() + "/index_test/";
     Index::Options options(index_dir_, /*index_merge_size=*/1024 * 1024,
                            /*lite_index_sort_at_indexing=*/true,
                            /*lite_index_sort_size=*/1024 * 8);
     ICING_ASSERT_OK_AND_ASSIGN(
-        index_, Index::Create(options, &filesystem_, &icing_filesystem_));
+        index_, Index::Create(options, &filesystem_, &icing_filesystem_,
+                              feature_flags_.get()));
   }
 
   void TearDown() override {
@@ -109,6 +115,7 @@
     return GetHits(std::move(itr));
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   IcingFilesystem icing_filesystem_;
   std::string index_dir_;
@@ -154,12 +161,15 @@
   Index::Options options(index_dir_, /*index_merge_size=*/1024 * 1024,
                          /*lite_index_sort_at_indexing=*/true,
                          /*lite_index_sort_size=*/1024 * 8);
-  EXPECT_THAT(
-      Index::Create(options, &filesystem_, /*icing_filesystem=*/nullptr),
-      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
-  EXPECT_THAT(
-      Index::Create(options, /*filesystem=*/nullptr, &icing_filesystem_),
-      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(Index::Create(options, &filesystem_, /*icing_filesystem=*/nullptr,
+                            feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(Index::Create(options, /*filesystem=*/nullptr, &icing_filesystem_,
+                            feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(Index::Create(options, &filesystem_, &icing_filesystem_,
+                            /*feature_flags=*/nullptr),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 }
 
 TEST_F(IndexTest, EmptyIndex) {
@@ -206,7 +216,8 @@
                          /*lite_index_sort_at_indexing=*/false,
                          /*lite_index_sort_size=*/16);
   ICING_ASSERT_OK_AND_ASSIGN(
-      index_, Index::Create(options, &filesystem_, &icing_filesystem_));
+      index_, Index::Create(options, &filesystem_, &icing_filesystem_,
+                            feature_flags_.get()));
 
   Index::Editor edit =
       index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
@@ -221,7 +232,8 @@
                            /*lite_index_sort_at_indexing=*/true,
                            /*lite_index_sort_size=*/16);
   ICING_ASSERT_OK_AND_ASSIGN(
-      index_, Index::Create(options, &filesystem_, &icing_filesystem_));
+      index_, Index::Create(options, &filesystem_, &icing_filesystem_,
+                            feature_flags_.get()));
 
   // Check that the index is sorted after recreating with
   // lite_index_sort_at_indexing, with the unsorted HitBuffer exceeding the sort
@@ -307,44 +319,48 @@
                           TermMatchType::EXACT_ONLY));
 
   // Before Advance().
-  EXPECT_THAT(
-      itr->GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/0,
-          /*num_leaf_advance_calls_main_index=*/0,
-          /*num_leaf_advance_calls_integer_index=*/0,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/0));
+  EXPECT_THAT(itr->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/0,
+                  /*num_leaf_advance_calls_main_index=*/0,
+                  /*num_leaf_advance_calls_integer_index=*/0,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/0,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 1st Advance().
   ICING_ASSERT_OK(itr->Advance());
-  EXPECT_THAT(
-      itr->GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/0,
-          /*num_leaf_advance_calls_main_index=*/1,
-          /*num_leaf_advance_calls_integer_index=*/0,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/1));
+  EXPECT_THAT(itr->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/0,
+                  /*num_leaf_advance_calls_main_index=*/1,
+                  /*num_leaf_advance_calls_integer_index=*/0,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/1,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 2nd Advance().
   ICING_ASSERT_OK(itr->Advance());
-  EXPECT_THAT(
-      itr->GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/0,
-          /*num_leaf_advance_calls_main_index=*/2,
-          /*num_leaf_advance_calls_integer_index=*/0,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/1));
+  EXPECT_THAT(itr->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/0,
+                  /*num_leaf_advance_calls_main_index=*/2,
+                  /*num_leaf_advance_calls_integer_index=*/0,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/1,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 3rd Advance().
   ASSERT_THAT(itr->Advance(),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
-  EXPECT_THAT(
-      itr->GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/0,
-          /*num_leaf_advance_calls_main_index=*/2,
-          /*num_leaf_advance_calls_integer_index=*/0,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/1));
+  EXPECT_THAT(itr->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/0,
+                  /*num_leaf_advance_calls_main_index=*/2,
+                  /*num_leaf_advance_calls_integer_index=*/0,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/1,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 }
 
 TEST_F(IndexTest, IteratorGetCallStats_liteIndexOnly) {
@@ -366,44 +382,48 @@
                           TermMatchType::EXACT_ONLY));
 
   // Before Advance().
-  EXPECT_THAT(
-      itr->GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/0,
-          /*num_leaf_advance_calls_main_index=*/0,
-          /*num_leaf_advance_calls_integer_index=*/0,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/0));
+  EXPECT_THAT(itr->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/0,
+                  /*num_leaf_advance_calls_main_index=*/0,
+                  /*num_leaf_advance_calls_integer_index=*/0,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/0,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 1st Advance().
   ICING_ASSERT_OK(itr->Advance());
-  EXPECT_THAT(
-      itr->GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/1,
-          /*num_leaf_advance_calls_main_index=*/0,
-          /*num_leaf_advance_calls_integer_index=*/0,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/0));
+  EXPECT_THAT(itr->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/1,
+                  /*num_leaf_advance_calls_main_index=*/0,
+                  /*num_leaf_advance_calls_integer_index=*/0,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/0,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 2nd Advance().
   ICING_ASSERT_OK(itr->Advance());
-  EXPECT_THAT(
-      itr->GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/2,
-          /*num_leaf_advance_calls_main_index=*/0,
-          /*num_leaf_advance_calls_integer_index=*/0,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/0));
+  EXPECT_THAT(itr->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/2,
+                  /*num_leaf_advance_calls_main_index=*/0,
+                  /*num_leaf_advance_calls_integer_index=*/0,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/0,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 3rd Advance().
   ASSERT_THAT(itr->Advance(),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
-  EXPECT_THAT(
-      itr->GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/2,
-          /*num_leaf_advance_calls_main_index=*/0,
-          /*num_leaf_advance_calls_integer_index=*/0,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/0));
+  EXPECT_THAT(itr->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/2,
+                  /*num_leaf_advance_calls_main_index=*/0,
+                  /*num_leaf_advance_calls_integer_index=*/0,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/0,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 }
 
 TEST_F(IndexTest, IteratorGetCallStats) {
@@ -439,72 +459,78 @@
                           TermMatchType::EXACT_ONLY));
 
   // Before Advance().
-  EXPECT_THAT(
-      itr->GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/0,
-          /*num_leaf_advance_calls_main_index=*/0,
-          /*num_leaf_advance_calls_integer_index=*/0,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/0));
+  EXPECT_THAT(itr->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/0,
+                  /*num_leaf_advance_calls_main_index=*/0,
+                  /*num_leaf_advance_calls_integer_index=*/0,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/0,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 1st Advance(). DocHitInfoIteratorOr will advance both left and right
   // iterator (i.e. lite and main index iterator) once, compare document ids,
   // and return the hit with larger document id. In this case, hit from lite
   // index will be chosen and returned.
   ICING_ASSERT_OK(itr->Advance());
-  EXPECT_THAT(
-      itr->GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/1,
-          /*num_leaf_advance_calls_main_index=*/1,
-          /*num_leaf_advance_calls_integer_index=*/0,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/1));
+  EXPECT_THAT(itr->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/1,
+                  /*num_leaf_advance_calls_main_index=*/1,
+                  /*num_leaf_advance_calls_integer_index=*/0,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/1,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 2nd Advance(). Since lite index iterator has larger document id in the
   // previous round, we advance lite index iterator in this round. We still
   // choose and return hit from lite index.
   ICING_ASSERT_OK(itr->Advance());
-  EXPECT_THAT(
-      itr->GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/2,
-          /*num_leaf_advance_calls_main_index=*/1,
-          /*num_leaf_advance_calls_integer_index=*/0,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/1));
+  EXPECT_THAT(itr->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/2,
+                  /*num_leaf_advance_calls_main_index=*/1,
+                  /*num_leaf_advance_calls_integer_index=*/0,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/1,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 3rd Advance(). Since lite index iterator has larger document id in the
   // previous round, we advance lite index iterator in this round. However,
   // there is no hit from lite index anymore, so we choose and return hit from
   // main index.
   ICING_ASSERT_OK(itr->Advance());
-  EXPECT_THAT(
-      itr->GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/2,
-          /*num_leaf_advance_calls_main_index=*/1,
-          /*num_leaf_advance_calls_integer_index=*/0,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/1));
+  EXPECT_THAT(itr->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/2,
+                  /*num_leaf_advance_calls_main_index=*/1,
+                  /*num_leaf_advance_calls_integer_index=*/0,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/1,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 4th Advance(). Advance main index.
   ICING_ASSERT_OK(itr->Advance());
-  EXPECT_THAT(
-      itr->GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/2,
-          /*num_leaf_advance_calls_main_index=*/2,
-          /*num_leaf_advance_calls_integer_index=*/0,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/1));
+  EXPECT_THAT(itr->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/2,
+                  /*num_leaf_advance_calls_main_index=*/2,
+                  /*num_leaf_advance_calls_integer_index=*/0,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/1,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 5th Advance(). Reach the end.
   ASSERT_THAT(itr->Advance(),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
-  EXPECT_THAT(
-      itr->GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/2,
-          /*num_leaf_advance_calls_main_index=*/2,
-          /*num_leaf_advance_calls_integer_index=*/0,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/1));
+  EXPECT_THAT(itr->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/2,
+                  /*num_leaf_advance_calls_main_index=*/2,
+                  /*num_leaf_advance_calls_integer_index=*/0,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/1,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 }
 
 TEST_F(IndexTest, SingleHitSingleTermIndex) {
@@ -1230,7 +1256,8 @@
                          /*lite_index_sort_at_indexing=*/true,
                          /*lite_index_sort_size=*/64);
   ICING_ASSERT_OK_AND_ASSIGN(
-      index_, Index::Create(options, &filesystem_, &icing_filesystem_));
+      index_, Index::Create(options, &filesystem_, &icing_filesystem_,
+                            feature_flags_.get()));
 
   std::default_random_engine random;
   std::vector<std::string> query_terms;
@@ -1299,7 +1326,8 @@
                          /*lite_index_sort_at_indexing=*/true,
                          /*lite_index_sort_size=*/64);
   ICING_ASSERT_OK_AND_ASSIGN(
-      index_, Index::Create(options, &filesystem_, &icing_filesystem_));
+      index_, Index::Create(options, &filesystem_, &icing_filesystem_,
+                            feature_flags_.get()));
 
   std::default_random_engine random;
   std::vector<std::string> query_terms;
@@ -1634,7 +1662,8 @@
   Index::Options options(index_dir_, /*index_merge_size=*/1024 * 1024,
                          /*lite_index_sort_at_indexing=*/true,
                          /*lite_index_sort_size=*/1024 * 8);
-  EXPECT_THAT(Index::Create(options, &filesystem_, &mock_icing_filesystem),
+  EXPECT_THAT(Index::Create(options, &filesystem_, &mock_icing_filesystem,
+                            feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INTERNAL));
 }
 
@@ -1667,7 +1696,8 @@
   Index::Options options(index_dir_, /*index_merge_size=*/1024 * 1024,
                          /*lite_index_sort_at_indexing=*/true,
                          /*lite_index_sort_size=*/1024 * 8);
-  EXPECT_THAT(Index::Create(options, &filesystem_, &icing_filesystem_),
+  EXPECT_THAT(Index::Create(options, &filesystem_, &icing_filesystem_,
+                            feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::DATA_LOSS));
 }
 
@@ -1719,7 +1749,8 @@
                          /*lite_index_sort_at_indexing=*/true,
                          /*lite_index_sort_size=*/1024 * 8);
   ICING_ASSERT_OK_AND_ASSIGN(
-      index_, Index::Create(options, &filesystem_, &icing_filesystem_));
+      index_, Index::Create(options, &filesystem_, &icing_filesystem_,
+                            feature_flags_.get()));
 
   // Check that the hits are present.
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1750,7 +1781,8 @@
                          /*lite_index_sort_at_indexing=*/true,
                          /*lite_index_sort_size=*/1024 * 8);
   ICING_ASSERT_OK_AND_ASSIGN(
-      index_, Index::Create(options, &filesystem_, &icing_filesystem_));
+      index_, Index::Create(options, &filesystem_, &icing_filesystem_,
+                            feature_flags_.get()));
 
   // Check that the hits are present.
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1767,7 +1799,8 @@
   Index::Options options(
       index_dir_, /*index_merge_size=*/std::numeric_limits<uint32_t>::max(),
       /*lite_index_sort_at_indexing=*/true, /*lite_index_sort_size=*/1024 * 8);
-  EXPECT_THAT(Index::Create(options, &filesystem_, &icing_filesystem_),
+  EXPECT_THAT(Index::Create(options, &filesystem_, &icing_filesystem_,
+                            feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
diff --git a/icing/index/integer-section-indexing-handler_test.cc b/icing/index/integer-section-indexing-handler_test.cc
index 0f70387..b1c7841 100644
--- a/icing/index/integer-section-indexing-handler_test.cc
+++ b/icing/index/integer-section-indexing-handler_test.cc
@@ -28,10 +28,12 @@
 #include "icing/document-builder.h"
 #include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
+#include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
 #include "icing/index/numeric/integer-index.h"
 #include "icing/index/numeric/numeric-index.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
@@ -169,14 +171,18 @@
         filesystem_.CreateDirectoryRecursively(document_store_dir_.c_str()));
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult doc_store_create_result,
-        DocumentStore::Create(&filesystem_, document_store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, document_store_dir_, &fake_clock_,
+            schema_store_.get(), feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(doc_store_create_result.document_store);
   }
 
@@ -232,11 +238,13 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(tokenized_document.document()));
+      document_store_->Put(tokenized_document.document_wrapper()));
 
   ASSERT_THAT(integer_index_->last_added_document_id(), Eq(kInvalidDocumentId));
   // Handle document.
@@ -284,11 +292,13 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(nested_document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(nested_document)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(tokenized_document.document()));
+      document_store_->Put(tokenized_document.document_wrapper()));
 
   ASSERT_THAT(integer_index_->last_added_document_id(), Eq(kInvalidDocumentId));
   // Handle nested_document.
@@ -349,11 +359,13 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(tokenized_document.document()));
+      document_store_->Put(tokenized_document.document_wrapper()));
 
   ASSERT_THAT(integer_index_->last_added_document_id(), Eq(kInvalidDocumentId));
   // Handle document. Index data should remain unchanged since there is no
@@ -392,9 +404,11 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
-  ICING_ASSERT_OK(document_store_->Put(tokenized_document.document()));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
+  ICING_ASSERT_OK(document_store_->Put(tokenized_document.document_wrapper()));
 
   static constexpr DocumentId kCurrentDocumentId = 3;
   integer_index_->set_last_added_document_id(kCurrentDocumentId);
@@ -453,11 +467,13 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(tokenized_document.document()));
+      document_store_->Put(tokenized_document.document_wrapper()));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<IntegerSectionIndexingHandler> handler,
@@ -531,18 +547,22 @@
           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document1,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document1)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document1)));
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document2,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document2)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document2)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(tokenized_document1.document()));
+      document_store_->Put(tokenized_document1.document_wrapper()));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(tokenized_document2.document()));
+      document_store_->Put(tokenized_document2.document_wrapper()));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<IntegerSectionIndexingHandler> handler,
diff --git a/icing/index/iterator/doc-hit-info-iterator-all-document-id.h b/icing/index/iterator/doc-hit-info-iterator-all-document-id.h
index 60c5e0c..53ecc38 100644
--- a/icing/index/iterator/doc-hit-info-iterator-all-document-id.h
+++ b/icing/index/iterator/doc-hit-info-iterator-all-document-id.h
@@ -15,11 +15,12 @@
 #ifndef ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_ALL_DOCUMENT_ID_H_
 #define ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_ALL_DOCUMENT_ID_H_
 
-#include <cstdint>
+#include <memory>
 #include <string>
+#include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
-#include "icing/absl_ports/str_cat.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
 #include "icing/legacy/core/icing-string-util.h"
 #include "icing/store/document-id.h"
@@ -29,7 +30,8 @@
 
 // Iterator for all DocumentIds in range [0, document_id_limit_]: 0 inclusive,
 // document_id_limit_ inclusive. Returns DocumentIds in descending order.
-class DocHitInfoIteratorAllDocumentId : public DocHitInfoIterator {
+class DocHitInfoIteratorAllDocumentId
+    : public DocHitInfoIteratorSectionRestrictionNotApplicable {
  public:
   explicit DocHitInfoIteratorAllDocumentId(DocumentId document_id_limit);
 
@@ -37,7 +39,9 @@
 
   libtextclassifier3::StatusOr<TrimmedNode> TrimRightMostNode() && override;
 
-  void MapChildren(const ChildrenMapper& mapper) override {}
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    return {};
+  }
 
   CallStats GetCallStats() const override {
     return CallStats(
@@ -46,7 +50,8 @@
         /*num_leaf_advance_calls_integer_index_in=*/0,
         /*num_leaf_advance_calls_no_index_in=*/document_id_limit_ -
             current_document_id_,
-        /*num_blocks_inspected_in=*/0);
+        /*num_blocks_inspected_in=*/0,
+        /*embedding_stats_in=*/{});
   }
 
   std::string ToString() const override {
diff --git a/icing/index/iterator/doc-hit-info-iterator-all-document-id_test.cc b/icing/index/iterator/doc-hit-info-iterator-all-document-id_test.cc
index 379cb4d..fc61efe 100644
--- a/icing/index/iterator/doc-hit-info-iterator-all-document-id_test.cc
+++ b/icing/index/iterator/doc-hit-info-iterator-all-document-id_test.cc
@@ -55,23 +55,25 @@
 
 TEST(DocHitInfoIteratorAllDocumentIdTest, GetCallStats) {
   DocHitInfoIteratorAllDocumentId all_it(100);
-  EXPECT_THAT(
-      all_it.GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/0,
-          /*num_leaf_advance_calls_main_index=*/0,
-          /*num_leaf_advance_calls_integer_index=*/0,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/0));
+  EXPECT_THAT(all_it.GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/0,
+                  /*num_leaf_advance_calls_main_index=*/0,
+                  /*num_leaf_advance_calls_integer_index=*/0,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/0,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   for (int i = 1; i <= 5; ++i) {
     EXPECT_THAT(all_it.Advance(), IsOk());
-    EXPECT_THAT(
-        all_it.GetCallStats(),
-        EqualsDocHitInfoIteratorCallStats(
-            /*num_leaf_advance_calls_lite_index=*/0,
-            /*num_leaf_advance_calls_main_index=*/0,
-            /*num_leaf_advance_calls_integer_index=*/0,
-            /*num_leaf_advance_calls_no_index=*/i, /*num_blocks_inspected=*/0));
+    EXPECT_THAT(all_it.GetCallStats(),
+                EqualsDocHitInfoIteratorCallStats(
+                    /*num_leaf_advance_calls_lite_index=*/0,
+                    /*num_leaf_advance_calls_main_index=*/0,
+                    /*num_leaf_advance_calls_integer_index=*/0,
+                    /*num_leaf_advance_calls_no_index=*/i,
+                    /*num_blocks_inspected=*/0,
+                    DocHitInfoIterator::CallStats::EmbeddingStats()));
   }
 }
 
diff --git a/icing/index/iterator/doc-hit-info-iterator-and.h b/icing/index/iterator/doc-hit-info-iterator-and.h
index 8c52ac9..963b380 100644
--- a/icing/index/iterator/doc-hit-info-iterator-and.h
+++ b/icing/index/iterator/doc-hit-info-iterator-and.h
@@ -15,14 +15,16 @@
 #ifndef ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_AND_H_
 #define ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_AND_H_
 
-#include <cstdint>
+#include <cstddef>
 #include <memory>
 #include <string>
-#include <utility>
 #include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/schema/section.h"
+#include "icing/store/document-id.h"
 
 namespace icing {
 namespace lib {
@@ -33,7 +35,8 @@
     std::vector<std::unique_ptr<DocHitInfoIterator>> iterators);
 
 // Iterate over a logical AND of two child iterators.
-class DocHitInfoIteratorAnd : public DocHitInfoIterator {
+class DocHitInfoIteratorAnd
+    : public DocHitInfoIteratorSectionRestrictionApplyToChildren {
  public:
   // Set the shorter iterator to short_it to get performance benefits
   // for when an underlying iterator has a more efficient AdvanceTo.
@@ -49,9 +52,8 @@
 
   std::string ToString() const override;
 
-  void MapChildren(const ChildrenMapper& mapper) override {
-    short_ = mapper(std::move(short_));
-    long_ = mapper(std::move(long_));
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    return {&short_, &long_};
   }
 
   void PopulateMatchedTermsStats(
@@ -75,7 +77,8 @@
 // Iterate over a logical AND of multiple child iterators.
 // NOTE: DocHitInfoIteratorAnd is a faster alternative to AND exactly 2
 // iterators.
-class DocHitInfoIteratorAndNary : public DocHitInfoIterator {
+class DocHitInfoIteratorAndNary
+    : public DocHitInfoIteratorSectionRestrictionApplyToChildren {
  public:
   explicit DocHitInfoIteratorAndNary(
       std::vector<std::unique_ptr<DocHitInfoIterator>> iterators);
@@ -88,10 +91,13 @@
 
   std::string ToString() const override;
 
-  void MapChildren(const ChildrenMapper& mapper) override {
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    std::vector<std::unique_ptr<DocHitInfoIterator>*> children;
+    children.reserve(iterators_.size());
     for (int i = 0; i < iterators_.size(); ++i) {
-      iterators_[i] = mapper(std::move(iterators_[i]));
+      children.push_back(&iterators_[i]);
     }
+    return children;
   }
 
   void PopulateMatchedTermsStats(
diff --git a/icing/index/iterator/doc-hit-info-iterator-and_test.cc b/icing/index/iterator/doc-hit-info-iterator-and_test.cc
index f204ada..f4faaf8 100644
--- a/icing/index/iterator/doc-hit-info-iterator-and_test.cc
+++ b/icing/index/iterator/doc-hit-info-iterator-and_test.cc
@@ -14,12 +14,16 @@
 
 #include "icing/index/iterator/doc-hit-info-iterator-and.h"
 
-#include <string>
+#include <memory>
+#include <unordered_map>
+#include <utility>
 #include <vector>
 
+#include "icing/text_classifier/lib3/utils/base/status.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "icing/index/hit/doc-hit-info.h"
+#include "icing/index/hit/hit.h"
 #include "icing/index/iterator/doc-hit-info-iterator-test-util.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
 #include "icing/schema/section.h"
@@ -34,6 +38,8 @@
 using ::testing::ElementsAre;
 using ::testing::Eq;
 using ::testing::IsEmpty;
+using ::testing::Pointee;
+using ::testing::Pointer;
 
 TEST(CreateAndIteratorTest, And) {
   // Basic test that we can create a working And iterator. Further testing of
@@ -84,7 +90,12 @@
       /*num_leaf_advance_calls_main_index_in=*/5,
       /*num_leaf_advance_calls_integer_index_in=*/3,
       /*num_leaf_advance_calls_no_index_in=*/1,
-      /*num_blocks_inspected_in=*/4);  // arbitrary value
+      /*num_blocks_inspected_in=*/4,
+      /*embedding_stats_in=*/
+      {.num_unquantized_embeddings_scored = 2,
+       .num_quantized_embeddings_scored = 3,
+       .unquantized_shards_read = {1, 2},
+       .quantized_shards_read{3, 4}});  // arbitrary value
   auto first_iter = std::make_unique<DocHitInfoIteratorDummy>();
   first_iter->SetCallStats(first_iter_call_stats);
 
@@ -93,7 +104,12 @@
       /*num_leaf_advance_calls_main_index_in=*/2,
       /*num_leaf_advance_calls_integer_index_in=*/10,
       /*num_leaf_advance_calls_no_index_in=*/3,
-      /*num_blocks_inspected_in=*/7);  // arbitrary value
+      /*num_blocks_inspected_in=*/7,
+      /*embedding_stats_in=*/
+      {.num_unquantized_embeddings_scored = 4,
+       .num_quantized_embeddings_scored = 5,
+       .unquantized_shards_read = {5, 6},
+       .quantized_shards_read{7}});  // arbitrary value
   auto second_iter = std::make_unique<DocHitInfoIteratorDummy>();
   second_iter->SetCallStats(second_iter_call_stats);
 
@@ -447,7 +463,12 @@
       /*num_leaf_advance_calls_main_index_in=*/5,
       /*num_leaf_advance_calls_integer_index_in=*/3,
       /*num_leaf_advance_calls_no_index_in=*/1,
-      /*num_blocks_inspected_in=*/4);  // arbitrary value
+      /*num_blocks_inspected_in=*/4,
+      /*embedding_stats_in=*/
+      {.num_unquantized_embeddings_scored = 2,
+       .num_quantized_embeddings_scored = 3,
+       .unquantized_shards_read = {1, 2},
+       .quantized_shards_read{3, 4}});  // arbitrary value
   auto first_iter = std::make_unique<DocHitInfoIteratorDummy>();
   first_iter->SetCallStats(first_iter_call_stats);
 
@@ -456,7 +477,12 @@
       /*num_leaf_advance_calls_main_index_in=*/2,
       /*num_leaf_advance_calls_integer_index_in=*/10,
       /*num_leaf_advance_calls_no_index_in=*/3,
-      /*num_blocks_inspected_in=*/7);  // arbitrary value
+      /*num_blocks_inspected_in=*/7,
+      /*embedding_stats_in=*/
+      {.num_unquantized_embeddings_scored = 4,
+       .num_quantized_embeddings_scored = 5,
+       .unquantized_shards_read = {5, 6},
+       .quantized_shards_read{7}});  // arbitrary value
   auto second_iter = std::make_unique<DocHitInfoIteratorDummy>();
   second_iter->SetCallStats(second_iter_call_stats);
 
@@ -465,7 +491,12 @@
       /*num_leaf_advance_calls_main_index_in=*/2000,
       /*num_leaf_advance_calls_integer_index_in=*/3000,
       /*num_leaf_advance_calls_no_index_in=*/0,
-      /*num_blocks_inspected_in=*/200);  // arbitrary value
+      /*num_blocks_inspected_in=*/200,
+      /*embedding_stats_in=*/
+      {.num_unquantized_embeddings_scored = 1,
+       .num_quantized_embeddings_scored = 1,
+       .unquantized_shards_read = {0},
+       .quantized_shards_read{0}});  // arbitrary value
   auto third_iter = std::make_unique<DocHitInfoIteratorDummy>();
   third_iter->SetCallStats(third_iter_call_stats);
 
@@ -474,7 +505,12 @@
       /*num_leaf_advance_calls_main_index_in=*/400,
       /*num_leaf_advance_calls_integer_index_in=*/100,
       /*num_leaf_advance_calls_no_index_in=*/20,
-      /*num_blocks_inspected_in=*/50);  // arbitrary value
+      /*num_blocks_inspected_in=*/50,
+      /*embedding_stats_in=*/
+      {.num_unquantized_embeddings_scored = 10,
+       .num_quantized_embeddings_scored = 10,
+       .unquantized_shards_read = {5, 6, 7},
+       .quantized_shards_read{9, 10, 11}});  // arbitrary value
   auto fourth_iter = std::make_unique<DocHitInfoIteratorDummy>();
   fourth_iter->SetCallStats(fourth_iter_call_stats);
 
@@ -630,6 +666,32 @@
   EXPECT_FALSE(and_iter.Advance().ok());
 }
 
+TEST(DocHitInfoIteratorAndNaryTest, GetChildren) {
+  std::vector<DocHitInfo> first_vector = {DocHitInfo(2), DocHitInfo(1),
+                                          DocHitInfo(0)};
+  std::vector<DocHitInfo> second_vector = {DocHitInfo(2), DocHitInfo(1)};
+  std::vector<DocHitInfo> third_vector = {DocHitInfo(2)};
+
+  std::vector<std::unique_ptr<DocHitInfoIterator>> iterators;
+  iterators.push_back(std::make_unique<DocHitInfoIteratorDummy>(first_vector));
+  iterators.push_back(std::make_unique<DocHitInfoIteratorDummy>(second_vector));
+  iterators.push_back(
+      std::make_unique<DocHitInfoIteratorDummy>(third_vector, "term", 10));
+
+  std::vector<DocHitInfoIterator*> iterator_ptrs;
+  for (const auto& iter : iterators) {
+    iterator_ptrs.push_back(iter.get());
+  }
+
+  std::unique_ptr<DocHitInfoIterator> iter =
+      std::make_unique<DocHitInfoIteratorAndNary>(std::move(iterators));
+
+  EXPECT_THAT(iter->GetChildren(),
+              ElementsAre(Pointee(Pointer(iterator_ptrs[0])),
+                          Pointee(Pointer(iterator_ptrs[1])),
+                          Pointee(Pointer(iterator_ptrs[2]))));
+}
+
 }  // namespace
 
 }  // namespace lib
diff --git a/icing/index/iterator/doc-hit-info-iterator-by-uri.h b/icing/index/iterator/doc-hit-info-iterator-by-uri.h
index d136535..9c8bb9f 100644
--- a/icing/index/iterator/doc-hit-info-iterator-by-uri.h
+++ b/icing/index/iterator/doc-hit-info-iterator-by-uri.h
@@ -31,7 +31,8 @@
 namespace icing {
 namespace lib {
 
-class DocHitInfoIteratorByUri : public DocHitInfoIterator {
+class DocHitInfoIteratorByUri
+    : public DocHitInfoIteratorSectionRestrictionNotApplicable {
  public:
   // Creates a DocHitInfoIteratorByUri based on the given search_spec.
   //
@@ -51,7 +52,9 @@
         "DocHitInfoIteratorByUri should not be used in suggestion.");
   }
 
-  void MapChildren(const ChildrenMapper& mapper) override {}
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    return {};
+  }
 
   CallStats GetCallStats() const override {
     return CallStats(
@@ -59,7 +62,8 @@
         /*num_leaf_advance_calls_main_index_in=*/0,
         /*num_leaf_advance_calls_integer_index_in=*/0,
         /*num_leaf_advance_calls_no_index_in=*/num_advance_calls_,
-        /*num_blocks_inspected_in=*/0);
+        /*num_blocks_inspected_in=*/0,
+        /*embedding_stats_in=*/{});
   }
 
   std::string ToString() const override { return "uri_iterator"; }
diff --git a/icing/index/iterator/doc-hit-info-iterator-by-uri_test.cc b/icing/index/iterator/doc-hit-info-iterator-by-uri_test.cc
index b783c48..058d13e 100644
--- a/icing/index/iterator/doc-hit-info-iterator-by-uri_test.cc
+++ b/icing/index/iterator/doc-hit-info-iterator-by-uri_test.cc
@@ -26,6 +26,7 @@
 #include "icing/file/filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/index/iterator/doc-hit-info-iterator-test-util.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/schema-builder.h"
@@ -36,6 +37,7 @@
 #include "icing/testing/fake-clock.h"
 #include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/document-util.h"
 
 namespace icing {
 namespace lib {
@@ -66,14 +68,18 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, test_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, test_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
   }
 
@@ -121,11 +127,16 @@
                                 .SetKey("namespace", "email/3")
                                 .SetSchema("email")
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document2));
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document3)));
   DocumentId document_id3 = put_result.new_document_id;
 
   // Create a search spec with uri filters that only match document1 and
@@ -158,12 +169,17 @@
                                 .SetKey("namespace", "email/3")
                                 .SetSchema("email")
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document3)));
   DocumentId document_id3 = put_result.new_document_id;
 
   // Create a search spec with uri filters that match all documents.
@@ -196,12 +212,17 @@
                                 .SetKey("namespace", "email/3")
                                 .SetSchema("email")
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document3)));
   DocumentId document_id3 = put_result.new_document_id;
 
   // Create a search spec with a nonexistent uri in uri filters.
@@ -235,10 +256,15 @@
                                 .SetKey("namespace", "email/3")
                                 .SetSchema("email")
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1));
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document2));
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document3)));
 
   // Create a search spec with all nonexistent uris.
   SearchSpecProto search_spec;
@@ -269,11 +295,16 @@
                                 .SetKey("namespace2", "email/3")
                                 .SetSchema("email")
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document2));
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document3)));
   DocumentId document_id3 = put_result.new_document_id;
 
   // Create a search spec with uri filters that match document1 and document3 in
@@ -314,14 +345,21 @@
                                 .SetKey("namespace", "email/4")
                                 .SetSchema("email")
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document3)));
   DocumentId document_id3 = put_result.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document4));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document4)));
   DocumentId document_id4 = put_result.new_document_id;
 
   // Create a search spec with duplicated uri filters. The result document ids
diff --git a/icing/index/iterator/doc-hit-info-iterator-data-holder.h b/icing/index/iterator/doc-hit-info-iterator-data-holder.h
new file mode 100644
index 0000000..f8c5ede
--- /dev/null
+++ b/icing/index/iterator/doc-hit-info-iterator-data-holder.h
@@ -0,0 +1,81 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_DATA_HOLDER_H_
+#define ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_DATA_HOLDER_H_
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/schema/section.h"
+#include "icing/util/status-macros.h"
+
+namespace icing {
+namespace lib {
+
+// An iterator that simply takes ownership of an object.
+template <typename T>
+class DocHitInfoIteratorDataHolder
+    : public DocHitInfoIteratorSectionRestrictionApplyToChildren {
+ public:
+  explicit DocHitInfoIteratorDataHolder(
+      std::unique_ptr<DocHitInfoIterator> delegate, std::unique_ptr<T> data)
+      : delegate_(std::move(delegate)), data_(std::move(data)) {}
+
+  libtextclassifier3::Status Advance() override {
+    auto result = delegate_->Advance();
+    doc_hit_info_ = delegate_->doc_hit_info();
+    return result;
+  }
+
+  libtextclassifier3::StatusOr<TrimmedNode> TrimRightMostNode() && override {
+    ICING_ASSIGN_OR_RETURN(TrimmedNode trimmed_delegate,
+                           std::move(*delegate_).TrimRightMostNode());
+    if (trimmed_delegate.iterator_ != nullptr) {
+      trimmed_delegate.iterator_ =
+          std::make_unique<DocHitInfoIteratorDataHolder>(
+              std::move(trimmed_delegate.iterator_), std::move(data_));
+    }
+    return trimmed_delegate;
+  }
+
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    return {&delegate_};
+  }
+
+  CallStats GetCallStats() const override { return delegate_->GetCallStats(); }
+
+  std::string ToString() const override { return delegate_->ToString(); }
+
+  void PopulateMatchedTermsStats(
+      std::vector<TermMatchInfo>* matched_terms_stats,
+      SectionIdMask filtering_section_mask) const override {
+    return delegate_->PopulateMatchedTermsStats(matched_terms_stats,
+                                                filtering_section_mask);
+  }
+
+ private:
+  std::unique_ptr<DocHitInfoIterator> delegate_;
+  std::unique_ptr<T> data_;
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_DATA_HOLDER_H_
diff --git a/icing/index/iterator/doc-hit-info-iterator-filter.cc b/icing/index/iterator/doc-hit-info-iterator-filter.cc
index 3bc3b48..477077d 100644
--- a/icing/index/iterator/doc-hit-info-iterator-filter.cc
+++ b/icing/index/iterator/doc-hit-info-iterator-filter.cc
@@ -14,11 +14,8 @@
 
 #include "icing/index/iterator/doc-hit-info-iterator-filter.h"
 
-#include <cstdint>
 #include <memory>
 #include <string>
-#include <string_view>
-#include <unordered_set>
 #include <utility>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
@@ -26,53 +23,63 @@
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
-#include "icing/schema/schema-store.h"
-#include "icing/store/document-filter-data.h"
+#include "icing/index/iterator/document-filter-predicate.h"
 #include "icing/store/document-id.h"
-#include "icing/store/document-store.h"
 #include "icing/util/status-macros.h"
 
 namespace icing {
 namespace lib {
 
-DocHitInfoIteratorFilter::DocHitInfoIteratorFilter(
-    std::unique_ptr<DocHitInfoIterator> delegate,
-    const DocumentStore* document_store, const SchemaStore* schema_store,
-    const Options& options, int64_t current_time_ms)
-    : delegate_(std::move(delegate)),
-      document_store_(*document_store),
-      schema_store_(*schema_store),
-      options_(options),
-      current_time_ms_(current_time_ms) {}
+// Some children iterators (currently only DocHitInfoIteratorEmbedding) can
+// internally handle the filter predicate to accelerate the search. Let's dfs to
+// all children iterators to try to provide the filter predicate.
+//
+// Returns true if all children iterators have accepted the filter predicate, in
+// which case we no longer need to apply the filter at the top level.
+bool PassFilterPredicateToChildrenAndHandle(
+    DocHitInfoIterator* iterator, const DocumentFilterPredicate* predicate) {
+  // If the predicate is accepted, we can stop here.
+  if (iterator->HandleFilter(predicate)) {
+    return true;
+  }
+
+  // Now, we know that the iterator cannot internally handle the filter
+  // predicate.
+  // If it has no children or the iterator cannot pass the filter predicate
+  // through (e.g., NOT iterator), return false to indicate that we should apply
+  // the filter at the top level.
+  if (iterator->GetChildren().empty() ||
+      !iterator->CanPassFilterPredicateThrough()) {
+    return false;
+  }
+
+  // Continue to pass the filter predicate to children iterators.
+  bool all_children_accepted = true;
+  for (std::unique_ptr<DocHitInfoIterator>* child : iterator->GetChildren()) {
+    all_children_accepted &=
+        PassFilterPredicateToChildrenAndHandle(child->get(), predicate);
+  }
+  return all_children_accepted;
+}
+
+/* static */ std::unique_ptr<DocHitInfoIterator>
+DocHitInfoIteratorFilter::ApplyFilter(
+    std::unique_ptr<DocHitInfoIterator> iterator,
+    const DocumentFilterPredicate* predicate,
+    bool enable_passing_filter_to_children) {
+  if (enable_passing_filter_to_children &&
+      PassFilterPredicateToChildrenAndHandle(iterator.get(), predicate)) {
+    return iterator;
+  }
+  return std::unique_ptr<DocHitInfoIteratorFilter>(
+      new DocHitInfoIteratorFilter(std::move(iterator), predicate));
+}
 
 libtextclassifier3::Status DocHitInfoIteratorFilter::Advance() {
   while (delegate_->Advance().ok()) {
-    // Try to get the DocumentFilterData
-    auto document_filter_data_optional =
-        document_store_.GetAliveDocumentFilterData(
-            delegate_->doc_hit_info().document_id(), current_time_ms_);
-    if (!document_filter_data_optional) {
-      // Didn't find the DocumentFilterData in the filter cache. This could be
-      // because the Document doesn't exist or the DocumentId isn't valid or the
-      // filter cache is in some invalid state. This is bad, but not the query's
-      // responsibility to fix, so just skip this result for now.
+    if (!(*predicate_)(delegate_->doc_hit_info().document_id())) {
       continue;
     }
-    // We should be guaranteed that this exists now.
-    DocumentFilterData data = document_filter_data_optional.value();
-
-    if (options_.filter_by_namespace_id_enabled &&
-        options_.target_namespace_ids.count(data.namespace_id()) == 0) {
-      // Doesn't match one of the specified namespaces. Keep searching
-      continue;
-    }
-
-    if (options_.filter_by_schema_type_id_enabled &&
-        options_.target_schema_type_ids.count(data.schema_type_id()) == 0) {
-      // Doesn't match one of the specified schema types. Keep searching
-      continue;
-    }
-
     // Satisfied all our specified filters
     doc_hit_info_ = delegate_->doc_hit_info();
     return libtextclassifier3::Status::OK;
@@ -88,9 +95,9 @@
   ICING_ASSIGN_OR_RETURN(TrimmedNode trimmed_delegate,
                          std::move(*delegate_).TrimRightMostNode());
   if (trimmed_delegate.iterator_ != nullptr) {
-    trimmed_delegate.iterator_ = std::make_unique<DocHitInfoIteratorFilter>(
-        std::move(trimmed_delegate.iterator_), &document_store_, &schema_store_,
-        options_, current_time_ms_);
+    trimmed_delegate.iterator_ =
+        std::unique_ptr<DocHitInfoIteratorFilter>(new DocHitInfoIteratorFilter(
+            std::move(trimmed_delegate.iterator_), predicate_));
   }
   return trimmed_delegate;
 }
diff --git a/icing/index/iterator/doc-hit-info-iterator-filter.h b/icing/index/iterator/doc-hit-info-iterator-filter.h
index b8e70e8..b937c22 100644
--- a/icing/index/iterator/doc-hit-info-iterator-filter.h
+++ b/icing/index/iterator/doc-hit-info-iterator-filter.h
@@ -15,62 +15,38 @@
 #ifndef ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_FILTER_H_
 #define ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_FILTER_H_
 
-#include <cstdint>
 #include <memory>
 #include <string>
-#include <unordered_set>
-#include <utility>
 #include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
-#include "icing/schema/schema-store.h"
+#include "icing/index/iterator/document-filter-predicate.h"
 #include "icing/schema/section.h"
-#include "icing/store/document-filter-data.h"
-#include "icing/store/document-store.h"
-#include "icing/store/namespace-id.h"
 
 namespace icing {
 namespace lib {
 
-// A iterator that helps filter out DocHitInfos associated with non-existing
-// document ids.
-class DocHitInfoIteratorFilter : public DocHitInfoIterator {
+// A iterator that helps filter out DocHitInfos by a given predicate.
+//
+// To maintain the correct semantics of section restrictions, it implements
+// DocHitInfoIteratorSectionRestrictionApplyToChildren to pass down section
+// restrictions to child iterators.
+class DocHitInfoIteratorFilter
+    : public DocHitInfoIteratorSectionRestrictionApplyToChildren {
  public:
-  struct Options {
-    // List of namespace ids that documents must have.
-    // filter_by_namespace_id_enabled=false means that all namespaces are valid,
-    // and no documents will be filtered out.
-    //
-    // Note that if we want to reference the strings in namespaces later, ensure
-    // that the caller who passed the Options class outlives the
-    // DocHitInfoIteratorFilter.
-    bool filter_by_namespace_id_enabled = false;
-    std::unordered_set<NamespaceId> target_namespace_ids;
-
-    // List of schema type ids that documents must have.
-    // filter_by_schema_type_id_enabled=false means that all schema types are
-    // valid, and no documents will be filtered out.
-    //
-    // Note that if we want to reference the strings in schema types later,
-    // ensure that the caller who passed the Options class outlives the
-    // DocHitInfoIteratorFilter.
-    bool filter_by_schema_type_id_enabled = false;
-    std::unordered_set<SchemaTypeId> target_schema_type_ids;
-  };
-
-  explicit DocHitInfoIteratorFilter(
-      std::unique_ptr<DocHitInfoIterator> delegate,
-      const DocumentStore* document_store, const SchemaStore* schema_store,
-      const Options& options, int64_t current_time_ms);
+  static std::unique_ptr<DocHitInfoIterator> ApplyFilter(
+      std::unique_ptr<DocHitInfoIterator> iterator,
+      const DocumentFilterPredicate* predicate,
+      bool enable_passing_filter_to_children);
 
   libtextclassifier3::Status Advance() override;
 
   libtextclassifier3::StatusOr<TrimmedNode> TrimRightMostNode() && override;
 
-  void MapChildren(const ChildrenMapper& mapper) override {
-    delegate_ = mapper(std::move(delegate_));
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    return {&delegate_};
   }
 
   CallStats GetCallStats() const override { return delegate_->GetCallStats(); }
@@ -85,11 +61,13 @@
   }
 
  private:
+  explicit DocHitInfoIteratorFilter(
+      std::unique_ptr<DocHitInfoIterator> delegate,
+      const DocumentFilterPredicate* predicate)
+      : delegate_(std::move(delegate)), predicate_(predicate) {}
+
   std::unique_ptr<DocHitInfoIterator> delegate_;
-  const DocumentStore& document_store_;
-  const SchemaStore& schema_store_;
-  const Options options_;
-  int64_t current_time_ms_;
+  const DocumentFilterPredicate* predicate_;
 };
 
 }  // namespace lib
diff --git a/icing/index/iterator/doc-hit-info-iterator-filter_test.cc b/icing/index/iterator/doc-hit-info-iterator-filter_test.cc
index 00cdb71..1f7d3b2 100644
--- a/icing/index/iterator/doc-hit-info-iterator-filter_test.cc
+++ b/icing/index/iterator/doc-hit-info-iterator-filter_test.cc
@@ -30,8 +30,12 @@
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator-and.h"
+#include "icing/index/iterator/doc-hit-info-iterator-not.h"
+#include "icing/index/iterator/doc-hit-info-iterator-or.h"
 #include "icing/index/iterator/doc-hit-info-iterator-test-util.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/index/iterator/document-filter-predicate.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/query/query-utils.h"
@@ -45,6 +49,7 @@
 #include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 #include "icing/util/clock.h"
+#include "icing/util/document-util.h"
 
 namespace icing {
 namespace lib {
@@ -54,6 +59,10 @@
 using ::testing::ElementsAre;
 using ::testing::Eq;
 using ::testing::IsEmpty;
+using ::testing::Ne;
+using ::testing::Pointee;
+using ::testing::Pointer;
+using ::testing::WhenDynamicCastTo;
 
 libtextclassifier3::StatusOr<DocumentStore::CreateResult> CreateDocumentStore(
     const Filesystem* filesystem, const std::string& base_dir,
@@ -64,9 +73,41 @@
       /*force_recovery_and_revalidate_documents=*/false,
       /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
       PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
+      PortableFileBackedProtoLog<
+          DocumentWrapper>::kDefaultCompressionThresholdBytes,
+      protobuf_ports::kDefaultMemLevel,
       /*initialize_stats=*/nullptr);
 }
 
+class DocHitInfoIteratorDummyHandlingFilter
+    : public DocHitInfoIteratorHandlingFilter {
+ public:
+  libtextclassifier3::Status Advance() override {
+    return libtextclassifier3::Status::OK;
+  }
+
+  libtextclassifier3::StatusOr<TrimmedNode> TrimRightMostNode() && override {
+    TrimmedNode node = {nullptr, /*term=*/"", /*term_start_index_=*/0,
+                        /*unnormalized_term_length_=*/0};
+    return node;
+  }
+
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    return {};
+  }
+
+  CallStats GetCallStats() const override { return CallStats(); }
+
+  std::string ToString() const override {
+    return "DocHitInfoIteratorDummyHandlingFilter";
+  }
+
+  std::vector<const DocumentFilterPredicate*> document_filter_predicates()
+      const {
+    return document_filter_predicates_;
+  }
+};
+
 class DocHitInfoIteratorDeletedFilterTest : public ::testing::Test {
  protected:
   DocHitInfoIteratorDeletedFilterTest()
@@ -97,6 +138,10 @@
         CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
                             schema_store_.get(), *feature_flags_));
     document_store_ = std::move(create_result.document_store);
+
+    predicate_ = GetFilterPredicateBySchemaAndNamespace(
+        SearchSpecProto::default_instance(), *document_store_, *schema_store_,
+        fake_clock_.GetSystemTimeMilliseconds());
   }
 
   void TearDown() override {
@@ -116,31 +161,39 @@
   DocumentProto test_document1_;
   DocumentProto test_document2_;
   DocumentProto test_document3_;
-  DocHitInfoIteratorFilter::Options options_;
+  std::unique_ptr<DocumentFilterPredicate> predicate_;
 };
 
 TEST_F(DocHitInfoIteratorDeletedFilterTest, EmptyOriginalIterator) {
-  ICING_ASSERT_OK(document_store_->Put(test_document1_));
+  ICING_ASSERT_OK(document_store_->Put(
+      document_util::CreateDocumentWrapper(test_document1_)));
 
   std::unique_ptr<DocHitInfoIterator> original_iterator_empty =
       std::make_unique<DocHitInfoIteratorDummy>();
 
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator_empty), document_store_.get(),
-      schema_store_.get(), options_, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator_empty), predicate_.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator), IsEmpty());
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()), IsEmpty());
 }
 
 TEST_F(DocHitInfoIteratorDeletedFilterTest, DeletedDocumentsAreFiltered) {
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(test_document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(test_document1_)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(test_document2_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(test_document2_)));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store_->Put(test_document3_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(test_document3_)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   // Deletes test document 2
@@ -154,23 +207,30 @@
   std::unique_ptr<DocHitInfoIterator> original_iterator =
       std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
 
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options_, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate_.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator),
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()),
               ElementsAre(document_id1, document_id3));
 }
 
 TEST_F(DocHitInfoIteratorDeletedFilterTest, NonExistingDocumentsAreFiltered) {
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(test_document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(test_document1_)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(test_document2_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(test_document2_)));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store_->Put(test_document3_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(test_document3_)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   // Document ids 7, 8, 9 are not existing
@@ -183,11 +243,12 @@
   std::unique_ptr<DocHitInfoIterator> original_iterator =
       std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
 
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options_, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate_.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator),
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()),
               ElementsAre(document_id1, document_id2, document_id3));
 }
 
@@ -196,11 +257,12 @@
   std::unique_ptr<DocHitInfoIterator> original_iterator =
       std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
 
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options_, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate_.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(filtered_iterator.Advance(),
+  EXPECT_THAT(filtered_iterator->Advance(),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
 }
 
@@ -210,11 +272,12 @@
   std::unique_ptr<DocHitInfoIterator> original_iterator =
       std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
 
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options_, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate_.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(filtered_iterator.Advance(),
+  EXPECT_THAT(filtered_iterator->Advance(),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
 }
 
@@ -227,11 +290,12 @@
   std::unique_ptr<DocHitInfoIterator> original_iterator =
       std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
 
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options_, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate_.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(filtered_iterator.Advance(),
+  EXPECT_THAT(filtered_iterator->Advance(),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
 }
 
@@ -305,19 +369,24 @@
       std::make_unique<DocHitInfoIteratorDummy>();
 
   SearchSpecProto search_spec;
-  DocHitInfoIteratorFilter::Options options =
-      GetFilterOptions(search_spec, *document_store_, *schema_store_);
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator_empty), document_store_.get(),
-      schema_store_.get(), options, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator_empty), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator), IsEmpty());
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()), IsEmpty());
 }
 
 TEST_F(DocHitInfoIteratorNamespaceFilterTest,
        NonexistentNamespacesReturnsEmpty) {
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1_namespace1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document1_namespace1_)));
   DocumentId document_id1 = put_result1.new_document_id;
   std::vector<DocHitInfo> doc_hit_infos = {DocHitInfo(document_id1)};
 
@@ -326,18 +395,23 @@
 
   SearchSpecProto search_spec;
   search_spec.add_namespace_filters("nonexistent_namespace");
-  DocHitInfoIteratorFilter::Options options =
-      GetFilterOptions(search_spec, *document_store_, *schema_store_);
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator), IsEmpty());
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()), IsEmpty());
 }
 
 TEST_F(DocHitInfoIteratorNamespaceFilterTest, NoNamespacesReturnsAll) {
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1_namespace1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document1_namespace1_)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   std::vector<DocHitInfo> doc_hit_infos = {DocHitInfo(document_id1)};
@@ -346,25 +420,35 @@
       std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
 
   SearchSpecProto search_spec;
-  DocHitInfoIteratorFilter::Options options =
-      GetFilterOptions(search_spec, *document_store_, *schema_store_);
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator), ElementsAre(document_id1));
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()),
+              ElementsAre(document_id1));
 }
 
 TEST_F(DocHitInfoIteratorNamespaceFilterTest,
        FilterOutExistingDocumentFromDifferentNamespace) {
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1_namespace1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document1_namespace1_)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2_namespace1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document2_namespace1_)));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store_->Put(document1_namespace2_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document1_namespace2_)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   std::vector<DocHitInfo> doc_hit_infos = {DocHitInfo(document_id1),
@@ -376,28 +460,39 @@
 
   SearchSpecProto search_spec;
   search_spec.add_namespace_filters(namespace1_);
-  DocHitInfoIteratorFilter::Options options =
-      GetFilterOptions(search_spec, *document_store_, *schema_store_);
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator),
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()),
               ElementsAre(document_id1, document_id2));
 }
 
 TEST_F(DocHitInfoIteratorNamespaceFilterTest, FilterForMultipleNamespacesOk) {
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1_namespace1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document1_namespace1_)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2_namespace1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document2_namespace1_)));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store_->Put(document1_namespace2_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document1_namespace2_)));
   DocumentId document_id3 = put_result3.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             document_store_->Put(document1_namespace3_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document1_namespace3_)));
   DocumentId document_id4 = put_result4.new_document_id;
 
   std::vector<DocHitInfo> doc_hit_infos = {
@@ -410,13 +505,16 @@
   SearchSpecProto search_spec;
   search_spec.add_namespace_filters(namespace1_);
   search_spec.add_namespace_filters(namespace3_);
-  DocHitInfoIteratorFilter::Options options =
-      GetFilterOptions(search_spec, *document_store_, *schema_store_);
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator),
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()),
               ElementsAre(document_id1, document_id2, document_id4));
 }
 
@@ -501,19 +599,24 @@
       std::make_unique<DocHitInfoIteratorDummy>();
 
   SearchSpecProto search_spec;
-  DocHitInfoIteratorFilter::Options options =
-      GetFilterOptions(search_spec, *document_store_, *schema_store_);
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator_empty), document_store_.get(),
-      schema_store_.get(), options, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator_empty), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator), IsEmpty());
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()), IsEmpty());
 }
 
 TEST_F(DocHitInfoIteratorSchemaTypeFilterTest,
        NonexistentSchemaTypeReturnsEmpty) {
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1_schema1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document1_schema1_)));
   DocumentId document_id1 = put_result1.new_document_id;
   std::vector<DocHitInfo> doc_hit_infos = {DocHitInfo(document_id1)};
 
@@ -522,18 +625,23 @@
 
   SearchSpecProto search_spec;
   search_spec.add_schema_type_filters("nonexistent_schema_type");
-  DocHitInfoIteratorFilter::Options options =
-      GetFilterOptions(search_spec, *document_store_, *schema_store_);
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator), IsEmpty());
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()), IsEmpty());
 }
 
 TEST_F(DocHitInfoIteratorSchemaTypeFilterTest, NoSchemaTypesReturnsAll) {
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1_schema1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document1_schema1_)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   std::vector<DocHitInfo> doc_hit_infos = {DocHitInfo(document_id1)};
@@ -542,22 +650,30 @@
       std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
 
   SearchSpecProto search_spec;
-  DocHitInfoIteratorFilter::Options options =
-      GetFilterOptions(search_spec, *document_store_, *schema_store_);
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator), ElementsAre(document_id1));
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()),
+              ElementsAre(document_id1));
 }
 
 TEST_F(DocHitInfoIteratorSchemaTypeFilterTest,
        FilterOutExistingDocumentFromDifferentSchemaTypes) {
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1_schema1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document1_schema1_)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2_schema2_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document2_schema2_)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   std::vector<DocHitInfo> doc_hit_infos = {DocHitInfo(document_id1),
@@ -568,24 +684,34 @@
 
   SearchSpecProto search_spec;
   search_spec.add_schema_type_filters(std::string(kSchema1));
-  DocHitInfoIteratorFilter::Options options =
-      GetFilterOptions(search_spec, *document_store_, *schema_store_);
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator), ElementsAre(document_id1));
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()),
+              ElementsAre(document_id1));
 }
 
 TEST_F(DocHitInfoIteratorSchemaTypeFilterTest, FilterForMultipleSchemaTypesOk) {
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1_schema1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document1_schema1_)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2_schema2_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document2_schema2_)));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store_->Put(document3_schema3_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document3_schema3_)));
   DocumentId document_id3 = put_result3.new_document_id;
   std::vector<DocHitInfo> doc_hit_infos = {DocHitInfo(document_id1),
                                            DocHitInfo(document_id2),
@@ -597,41 +723,50 @@
   SearchSpecProto search_spec;
   search_spec.add_schema_type_filters(std::string(kSchema2));
   search_spec.add_schema_type_filters(std::string(kSchema3));
-  DocHitInfoIteratorFilter::Options options =
-      GetFilterOptions(search_spec, *document_store_, *schema_store_);
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator),
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()),
               ElementsAre(document_id2, document_id3));
 }
 
 TEST_F(DocHitInfoIteratorSchemaTypeFilterTest,
        FilterIsExactForSchemaTypePolymorphism) {
   // Add some irrelevant documents.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1_schema1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document1_schema1_)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2_schema2_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document2_schema2_)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Create a person document and an artist document, where the artist should be
   // able to be interpreted as a person by polymorphism.
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult person_put_result,
-      document_store_->Put(DocumentBuilder()
-                               .SetKey("namespace", "person")
-                               .SetSchema("person")
-                               .Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder()
+              .SetKey("namespace", "person")
+              .SetSchema("person")
+              .Build())));
   DocumentId person_document_id = person_put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult artist_put_result,
-      document_store_->Put(DocumentBuilder()
-                               .SetKey("namespace", "artist")
-                               .SetSchema("artist")
-                               .Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder()
+              .SetKey("namespace", "artist")
+              .SetSchema("artist")
+              .Build())));
   DocumentId artist_document_id = artist_put_result.new_document_id;
 
   std::vector<DocHitInfo> doc_hit_infos = {
@@ -644,23 +779,29 @@
       std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
   SearchSpecProto search_spec_1;
   search_spec_1.add_schema_type_filters("person");
-  DocHitInfoIteratorFilter::Options options =
-      GetFilterOptions(search_spec_1, *document_store_, *schema_store_);
-  DocHitInfoIteratorFilter filtered_iterator_1(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator_1),
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec_1, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator_1 =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
+  EXPECT_THAT(GetDocumentIds(filtered_iterator_1.get()),
               ElementsAre(person_document_id));
 
   // Filters for the "artist" type should not include the "person" type.
   original_iterator = std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
   SearchSpecProto search_spec_2;
   search_spec_2.add_schema_type_filters("artist");
-  options = GetFilterOptions(search_spec_2, *document_store_, *schema_store_);
-  DocHitInfoIteratorFilter filtered_iterator_2(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator_2),
+  predicate = GetFilterPredicateBySchemaAndNamespace(
+      search_spec_2, *document_store_, *schema_store_,
+      fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator_2 =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
+  EXPECT_THAT(GetDocumentIds(filtered_iterator_2.get()),
               ElementsAre(artist_document_id));
 }
 
@@ -669,27 +810,30 @@
   // Create an email and a message document.
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_put_result,
-      document_store_->Put(DocumentBuilder()
-                               .SetKey("namespace", "email")
-                               .SetSchema("email")
-                               .Build()));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "email")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId email_document_id = email_put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult message_put_result,
-      document_store_->Put(DocumentBuilder()
-                               .SetKey("namespace", "message")
-                               .SetSchema("message")
-                               .Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder()
+              .SetKey("namespace", "message")
+              .SetSchema("message")
+              .Build())));
   DocumentId message_document_id = message_put_result.new_document_id;
 
   // Create a emailMessage document, which the should be able to be interpreted
   // as both an email and a message by polymorphism.
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_message_put_result,
-      document_store_->Put(DocumentBuilder()
-                               .SetKey("namespace", "emailMessage")
-                               .SetSchema("emailMessage")
-                               .Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder()
+              .SetKey("namespace", "emailMessage")
+              .SetSchema("emailMessage")
+              .Build())));
   DocumentId email_message_document_id =
       email_message_put_result.new_document_id;
 
@@ -703,12 +847,15 @@
       std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
   SearchSpecProto search_spec_1;
   search_spec_1.add_schema_type_filters("email");
-  DocHitInfoIteratorFilter::Options options =
-      GetFilterOptions(search_spec_1, *document_store_, *schema_store_);
-  DocHitInfoIteratorFilter filtered_iterator_1(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator_1),
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec_1, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator_1 =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
+  EXPECT_THAT(GetDocumentIds(filtered_iterator_1.get()),
               ElementsAre(email_document_id));
 
   // Filters for the "message" type should NOT include the "emailMessage" type,
@@ -716,22 +863,28 @@
   original_iterator = std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
   SearchSpecProto search_spec_2;
   search_spec_2.add_schema_type_filters("message");
-  options = GetFilterOptions(search_spec_2, *document_store_, *schema_store_);
-  DocHitInfoIteratorFilter filtered_iterator_2(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator_2),
+  predicate = GetFilterPredicateBySchemaAndNamespace(
+      search_spec_2, *document_store_, *schema_store_,
+      fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator_2 =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
+  EXPECT_THAT(GetDocumentIds(filtered_iterator_2.get()),
               ElementsAre(message_document_id));
 
   // Filters for a irrelevant type should return nothing.
   original_iterator = std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
   SearchSpecProto search_spec_3;
   search_spec_3.add_schema_type_filters("person");
-  options = GetFilterOptions(search_spec_3, *document_store_, *schema_store_);
-  DocHitInfoIteratorFilter filtered_iterator_3(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator_3), IsEmpty());
+  predicate = GetFilterPredicateBySchemaAndNamespace(
+      search_spec_3, *document_store_, *schema_store_,
+      fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator_3 =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
+  EXPECT_THAT(GetDocumentIds(filtered_iterator_3.get()), IsEmpty());
 }
 
 class DocHitInfoIteratorExpirationFilterTest : public ::testing::Test {
@@ -758,6 +911,10 @@
         CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
                             schema_store_.get(), *feature_flags_));
     document_store_ = std::move(create_result.document_store);
+
+    predicate_ = GetFilterPredicateBySchemaAndNamespace(
+        SearchSpecProto::default_instance(), *document_store_, *schema_store_,
+        fake_clock_.GetSystemTimeMilliseconds());
   }
 
   void TearDown() override {
@@ -775,12 +932,15 @@
   const Filesystem filesystem_;
   const std::string test_dir_;
   const std::string email_schema_ = "email";
-  DocHitInfoIteratorFilter::Options options_;
+  std::unique_ptr<DocumentFilterPredicate> predicate_;
 };
 
 TEST_F(DocHitInfoIteratorExpirationFilterTest, TtlZeroIsntFilteredOut) {
   // Arbitrary value
   fake_clock_.SetSystemTimeMilliseconds(100);
+  predicate_ = GetFilterPredicateBySchemaAndNamespace(
+      SearchSpecProto::default_instance(), *document_store_, *schema_store_,
+      fake_clock_.GetSystemTimeMilliseconds());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
@@ -796,24 +956,30 @@
                                .SetCreationTimestampMs(0)
                                .SetTtlMs(0)
                                .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   std::vector<DocHitInfo> doc_hit_infos = {DocHitInfo(document_id1)};
   std::unique_ptr<DocHitInfoIterator> original_iterator =
       std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
 
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store.get(), schema_store_.get(),
-      options_, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate_.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator), ElementsAre(document_id1));
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()),
+              ElementsAre(document_id1));
 }
 
 TEST_F(DocHitInfoIteratorExpirationFilterTest, BeforeTtlNotFilteredOut) {
   // Arbitrary value, but must be less than document's creation_timestamp + ttl
   fake_clock_.SetSystemTimeMilliseconds(50);
+  predicate_ = GetFilterPredicateBySchemaAndNamespace(
+      SearchSpecProto::default_instance(), *document_store_, *schema_store_,
+      fake_clock_.GetSystemTimeMilliseconds());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
@@ -829,24 +995,30 @@
                                .SetCreationTimestampMs(1)
                                .SetTtlMs(100)
                                .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   std::vector<DocHitInfo> doc_hit_infos = {DocHitInfo(document_id1)};
   std::unique_ptr<DocHitInfoIterator> original_iterator =
       std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
 
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store.get(), schema_store_.get(),
-      options_, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate_.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator), ElementsAre(document_id1));
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()),
+              ElementsAre(document_id1));
 }
 
 TEST_F(DocHitInfoIteratorExpirationFilterTest, EqualTtlFilteredOut) {
   // Current time is exactly the document's creation_timestamp + ttl
   fake_clock_.SetSystemTimeMilliseconds(150);
+  predicate_ = GetFilterPredicateBySchemaAndNamespace(
+      SearchSpecProto::default_instance(), *document_store_, *schema_store_,
+      fake_clock_.GetSystemTimeMilliseconds());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
@@ -862,25 +1034,30 @@
                                .SetCreationTimestampMs(50)
                                .SetTtlMs(100)
                                .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   std::vector<DocHitInfo> doc_hit_infos = {DocHitInfo(document_id1)};
   std::unique_ptr<DocHitInfoIterator> original_iterator =
       std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
 
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store.get(), schema_store_.get(),
-      options_, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate_.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator), IsEmpty());
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()), IsEmpty());
 }
 
 TEST_F(DocHitInfoIteratorExpirationFilterTest, PastTtlFilteredOut) {
   // Arbitrary value, but must be greater than the document's
   // creation_timestamp + ttl
   fake_clock_.SetSystemTimeMilliseconds(151);
+  predicate_ = GetFilterPredicateBySchemaAndNamespace(
+      SearchSpecProto::default_instance(), *document_store_, *schema_store_,
+      fake_clock_.GetSystemTimeMilliseconds());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
@@ -896,19 +1073,21 @@
                                .SetCreationTimestampMs(50)
                                .SetTtlMs(100)
                                .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   std::vector<DocHitInfo> doc_hit_infos = {DocHitInfo(document_id1)};
   std::unique_ptr<DocHitInfoIterator> original_iterator =
       std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
 
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store.get(), schema_store_.get(),
-      options_, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate_.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator), IsEmpty());
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()), IsEmpty());
 }
 
 class DocHitInfoIteratorFilterTest : public ::testing::Test {
@@ -1005,23 +1184,28 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store->Put(document1_namespace1_schema1_));
+      document_store->Put(
+          document_util::CreateDocumentWrapper(document1_namespace1_schema1_)));
   DocumentId document_id1 = put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store->Put(document2_namespace1_schema1_));
+      document_store->Put(
+          document_util::CreateDocumentWrapper(document2_namespace1_schema1_)));
   DocumentId document_id2 = put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
-      document_store->Put(document3_namespace2_schema1_));
+      document_store->Put(
+          document_util::CreateDocumentWrapper(document3_namespace2_schema1_)));
   DocumentId document_id3 = put_result3.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result4,
-      document_store->Put(document4_namespace1_schema2_));
+      document_store->Put(
+          document_util::CreateDocumentWrapper(document4_namespace1_schema2_)));
   DocumentId document_id4 = put_result4.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result5,
-      document_store->Put(document5_namespace1_schema1_));
+      document_store->Put(
+          document_util::CreateDocumentWrapper(document5_namespace1_schema1_)));
   DocumentId document_id5 = put_result5.new_document_id;
 
   // Deletes document2, causing it to be filtered out
@@ -1043,28 +1227,35 @@
   search_spec.add_namespace_filters(namespace1_);
   // Filters out document4 by schema type
   search_spec.add_schema_type_filters(schema1_);
-  DocHitInfoIteratorFilter::Options options =
-      GetFilterOptions(search_spec, *document_store, *schema_store_);
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
 
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocumentIds(&filtered_iterator), ElementsAre(document_id1));
+  EXPECT_THAT(GetDocumentIds(filtered_iterator.get()),
+              ElementsAre(document_id1));
 }
 
 TEST_F(DocHitInfoIteratorFilterTest, SectionIdMasksArePopulatedCorrectly) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(document1_namespace1_schema1_));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document1_namespace1_schema1_)));
   DocumentId document_id1 = put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(document2_namespace1_schema1_));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document2_namespace1_schema1_)));
   DocumentId document_id2 = put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
-      document_store_->Put(document3_namespace2_schema1_));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document3_namespace2_schema1_)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   SectionIdMask section_id_mask1 = 0b01001001;  // hits in sections 0, 3, 6
@@ -1081,12 +1272,16 @@
   std::unique_ptr<DocHitInfoIterator> original_iterator =
       std::make_unique<DocHitInfoIteratorDummy>(doc_hit_infos);
 
-  DocHitInfoIteratorFilter::Options options;
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          SearchSpecProto::default_instance(), *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(GetDocHitInfos(&filtered_iterator),
+  EXPECT_THAT(GetDocHitInfos(filtered_iterator.get()),
               ElementsAre(EqualsDocHitInfo(document_id1, section_ids1),
                           EqualsDocHitInfo(document_id2, section_ids2),
                           EqualsDocHitInfo(document_id3, section_ids3)));
@@ -1098,30 +1293,42 @@
       /*num_leaf_advance_calls_main_index_in=*/5,
       /*num_leaf_advance_calls_integer_index_in=*/3,
       /*num_leaf_advance_calls_no_index_in=*/1,
-      /*num_blocks_inspected_in=*/4);  // arbitrary value
+      /*num_blocks_inspected_in=*/4,
+      /*embedding_stats_in=*/
+      {.num_unquantized_embeddings_scored = 2,
+       .num_quantized_embeddings_scored = 3,
+       .unquantized_shards_read = {1, 2},
+       .quantized_shards_read{3, 4}});  // arbitrary value
   auto original_iterator = std::make_unique<DocHitInfoIteratorDummy>();
   original_iterator->SetCallStats(original_call_stats);
 
-  DocHitInfoIteratorFilter::Options options;
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          SearchSpecProto::default_instance(), *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
-  EXPECT_THAT(filtered_iterator.GetCallStats(), Eq(original_call_stats));
+  EXPECT_THAT(filtered_iterator->GetCallStats(), Eq(original_call_stats));
 }
 
 TEST_F(DocHitInfoIteratorFilterTest, TrimFilterIterator) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(document1_namespace1_schema1_));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document1_namespace1_schema1_)));
   DocumentId document_id1 = put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(document2_namespace1_schema1_));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document2_namespace1_schema1_)));
   DocumentId document_id2 = put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
-      document_store_->Put(document3_namespace2_schema1_));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document3_namespace2_schema1_)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   // Build an interator tree like:
@@ -1146,24 +1353,549 @@
   SearchSpecProto search_spec;
   // Filters out document3 by namespace
   search_spec.add_namespace_filters(namespace1_);
-  DocHitInfoIteratorFilter::Options options =
-      GetFilterOptions(search_spec, *document_store_, *schema_store_);
-  DocHitInfoIteratorFilter filtered_iterator(
-      std::move(original_iterator), document_store_.get(), schema_store_.get(),
-      options, fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  std::unique_ptr<DocHitInfoIterator> filtered_iterator =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_iterator), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
 
   // The trimmed tree.
   //          Filter
   //             |
   //          {1, 3}
   ICING_ASSERT_OK_AND_ASSIGN(DocHitInfoIterator::TrimmedNode trimmed_node,
-                             std::move(filtered_iterator).TrimRightMostNode());
+                             std::move(*filtered_iterator).TrimRightMostNode());
   EXPECT_THAT(trimmed_node.term_, Eq("term"));
   EXPECT_THAT(trimmed_node.term_start_index_, Eq(10));
   EXPECT_THAT(GetDocumentIds(trimmed_node.iterator_.get()),
               ElementsAre(document_id1));
 }
 
+TEST_F(DocHitInfoIteratorFilterTest,
+       ApplyFilterReturnsOriginalWhenRootHandles) {
+  SearchSpecProto search_spec_1;
+  search_spec_1.add_schema_type_filters(schema1_);
+  search_spec_1.add_namespace_filters(namespace1_);
+  std::unique_ptr<DocumentFilterPredicate> predicate_1 =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec_1, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  SearchSpecProto search_spec_2;
+  search_spec_2.add_schema_type_filters(schema2_);
+  search_spec_2.add_namespace_filters(namespace2_);
+  std::unique_ptr<DocumentFilterPredicate> predicate_2 =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec_2, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+
+  // Apply predicate_1 to an iterator that can handle filter internally.
+  std::unique_ptr<DocHitInfoIteratorDummyHandlingFilter> original_root =
+      std::make_unique<DocHitInfoIteratorDummyHandlingFilter>();
+  DocHitInfoIteratorDummyHandlingFilter* original_root_ptr =
+      original_root.get();
+  std::unique_ptr<DocHitInfoIterator> new_root =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_root), predicate_1.get(),
+          feature_flags_->enable_passing_filter_to_children());
+  // The filter is applied internally, so the pointer should not change.
+  EXPECT_THAT(new_root.get(), Eq(original_root_ptr));
+  // new_root is not a DocHitInfoIteratorFilter.
+  EXPECT_THAT(new_root.get(),
+              WhenDynamicCastTo<DocHitInfoIteratorFilter*>(nullptr));
+  // Check that the predicate is applied correctly.
+  EXPECT_THAT(original_root_ptr->document_filter_predicates(),
+              ElementsAre(predicate_1.get()));
+
+  // Apply predicate_2 to the same iterator.
+  new_root = DocHitInfoIteratorFilter::ApplyFilter(
+      std::move(new_root), predicate_2.get(),
+      feature_flags_->enable_passing_filter_to_children());
+  // The pointer should not change for the same reason.
+  EXPECT_THAT(new_root.get(), Eq(original_root_ptr));
+  // new_root is not a DocHitInfoIteratorFilter.
+  EXPECT_THAT(new_root.get(),
+              WhenDynamicCastTo<DocHitInfoIteratorFilter*>(nullptr));
+  // Check that the predicates are applied correctly.
+  EXPECT_THAT(original_root_ptr->document_filter_predicates(),
+              ElementsAre(predicate_1.get(), predicate_2.get()));
+}
+
+TEST_F(DocHitInfoIteratorFilterTest,
+       ApplyFilterReturnsOriginalWhenAllChildrenHandle) {
+  // Create an iterator tree:
+  //                        AND (root)
+  //                         /    \
+  //       DummyHandlingFilter    DummyHandlingFilter
+  std::unique_ptr<DocHitInfoIteratorDummyHandlingFilter> child1 =
+      std::make_unique<DocHitInfoIteratorDummyHandlingFilter>();
+  std::unique_ptr<DocHitInfoIteratorDummyHandlingFilter> child2 =
+      std::make_unique<DocHitInfoIteratorDummyHandlingFilter>();
+  DocHitInfoIteratorDummyHandlingFilter* child1_ptr = child1.get();
+  DocHitInfoIteratorDummyHandlingFilter* child2_ptr = child2.get();
+  std::unique_ptr<DocHitInfoIterator> original_root =
+      std::make_unique<DocHitInfoIteratorAnd>(std::move(child1),
+                                              std::move(child2));
+  DocHitInfoIterator* original_root_ptr = original_root.get();
+
+  // Test that AND iterator can pass predicate through, and since all children
+  // can handle the filter, the original iterator is returned.
+  SearchSpecProto search_spec;
+  search_spec.add_schema_type_filters(schema1_);
+  search_spec.add_namespace_filters(namespace1_);
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  // We should get the same tree:
+  //                        AND (root)
+  //                         /    \
+  //       DummyHandlingFilter    DummyHandlingFilter
+  std::unique_ptr<DocHitInfoIterator> new_root =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_root), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
+
+  // The filter should be handled by children, returning the original iterator.
+  EXPECT_THAT(new_root.get(), Eq(original_root_ptr));
+  EXPECT_THAT(new_root.get(),
+              WhenDynamicCastTo<DocHitInfoIteratorFilter*>(nullptr));
+  // Verify the predicate was passed down to children.
+  EXPECT_THAT(child1_ptr->document_filter_predicates(),
+              ElementsAre(predicate.get()));
+  EXPECT_THAT(child2_ptr->document_filter_predicates(),
+              ElementsAre(predicate.get()));
+}
+
+TEST_F(DocHitInfoIteratorFilterTest,
+       ApplyFilterReturnsNewWhenSomeChildrenHandle) {
+  // Create an iterator tree:
+  //                        AND (root)
+  //                         /    \
+  //       DummyHandlingFilter    Dummy
+  std::unique_ptr<DocHitInfoIteratorDummyHandlingFilter> child1 =
+      std::make_unique<DocHitInfoIteratorDummyHandlingFilter>();
+  std::unique_ptr<DocHitInfoIteratorDummy> child2 =
+      std::make_unique<DocHitInfoIteratorDummy>();
+  DocHitInfoIteratorDummyHandlingFilter* child1_ptr = child1.get();
+  std::unique_ptr<DocHitInfoIterator> original_root =
+      std::make_unique<DocHitInfoIteratorAnd>(std::move(child1),
+                                              std::move(child2));
+  DocHitInfoIterator* original_root_ptr = original_root.get();
+
+  // Create a predicate.
+  SearchSpecProto search_spec;
+  search_spec.add_schema_type_filters(schema1_);
+  search_spec.add_namespace_filters(namespace1_);
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+
+  // Apply the filter. Since not all children can handle the filter, a new
+  // filter iterator should be created at the top. However, DummyHandlingFilter
+  // should still get the predicate and try to handle it internally.
+  //
+  // We should get a new tree:
+  //                      Filter (root)
+  //                           |
+  //                          AND
+  //                         /   \
+  //       DummyHandlingFilter   Dummy
+  std::unique_ptr<DocHitInfoIterator> new_root =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_root), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
+
+  // The filter should be applied at the top level, so the pointer should
+  // change and be of type DocHitInfoIteratorFilter.
+  EXPECT_THAT(new_root.get(), Ne(original_root_ptr));
+  ASSERT_THAT(new_root.get(),
+              WhenDynamicCastTo<DocHitInfoIteratorFilter*>(Ne(nullptr)));
+  // Check that the original iterator is now the delegate.
+  EXPECT_THAT(new_root->GetChildren(),
+              ElementsAre(Pointee(Pointer(original_root_ptr))));
+  // Verify the predicate was passed down to the child that can handle it.
+  EXPECT_THAT(child1_ptr->document_filter_predicates(),
+              ElementsAre(predicate.get()));
+  // The other child (Dummy) doesn't store predicates, so no check needed there.
+}
+
+TEST_F(DocHitInfoIteratorFilterTest,
+       ApplyFilterReturnsNewWhenNoChildrenHandle) {
+  // Create an iterator tree:
+  //                        AND (root)
+  //                         /    \
+  //                     Dummy    Dummy
+  std::unique_ptr<DocHitInfoIterator> original_root =
+      std::make_unique<DocHitInfoIteratorAnd>(
+          std::make_unique<DocHitInfoIteratorDummy>(),
+          std::make_unique<DocHitInfoIteratorDummy>());
+  DocHitInfoIterator* original_root_ptr = original_root.get();
+
+  // Create a predicate.
+  SearchSpecProto search_spec;
+  search_spec.add_schema_type_filters(schema1_);
+  search_spec.add_namespace_filters(namespace1_);
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+
+  // Apply the filter. Since no children can handle the filter, a new filter
+  // iterator should be created at the top.
+  //
+  // We should get a new tree:
+  //                      Filter (root)
+  //                           |
+  //                          AND
+  //                         /   \
+  //                     Dummy   Dummy
+  std::unique_ptr<DocHitInfoIterator> new_root =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_root), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
+
+  // The filter should be applied at the top level, so the pointer should
+  // change and be of type DocHitInfoIteratorFilter.
+  EXPECT_THAT(new_root.get(), Ne(original_root_ptr));
+  ASSERT_THAT(new_root.get(),
+              WhenDynamicCastTo<DocHitInfoIteratorFilter*>(Ne(nullptr)));
+  // Check that the original iterator is now the delegate.
+  EXPECT_THAT(new_root->GetChildren(),
+              ElementsAre(Pointee(Pointer(original_root_ptr))));
+}
+
+TEST_F(DocHitInfoIteratorFilterTest,
+       ApplyFilterReturnsNewWhenRootHasNoChildren) {
+  // Create a root iterator that cannot handle filters and has no children.
+  std::unique_ptr<DocHitInfoIterator> original_root =
+      std::make_unique<DocHitInfoIteratorDummy>();
+  DocHitInfoIterator* original_root_ptr = original_root.get();
+
+  // Create a predicate.
+  SearchSpecProto search_spec;
+  search_spec.add_schema_type_filters(schema1_);
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+
+  // Apply the filter. Since the root cannot handle the filter and has no
+  // children, a new filter iterator should be created at the top.
+  //
+  // We should get a new tree:
+  //                      Filter (root)
+  //                           |
+  //                         Dummy
+  std::unique_ptr<DocHitInfoIterator> new_root =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_root), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
+
+  // The filter should be applied at the top level, so the pointer should
+  // change and be of type DocHitInfoIteratorFilter.
+  EXPECT_THAT(new_root.get(), Ne(original_root_ptr));
+  ASSERT_THAT(new_root.get(),
+              WhenDynamicCastTo<DocHitInfoIteratorFilter*>(Ne(nullptr)));
+  // Check that the original iterator is now the delegate.
+  EXPECT_THAT(new_root->GetChildren(),
+              ElementsAre(Pointee(Pointer(original_root_ptr))));
+}
+
+TEST_F(DocHitInfoIteratorFilterTest,
+       ApplyFilterToIteratorThatCannotPassPredicateThrough) {
+  // Create an iterator tree:
+  //                NOT (root)
+  //                 |
+  //         DummyHandlingFilter
+  std::unique_ptr<DocHitInfoIteratorDummyHandlingFilter>
+      iterator_handling_filter =
+          std::make_unique<DocHitInfoIteratorDummyHandlingFilter>();
+  DocHitInfoIteratorDummyHandlingFilter* iterator_handling_filter_ptr =
+      iterator_handling_filter.get();
+  std::unique_ptr<DocHitInfoIterator> original_root =
+      std::make_unique<DocHitInfoIteratorNot>(
+          std::move(iterator_handling_filter),
+          /*document_id_limit=*/100);
+  DocHitInfoIterator* original_root_ptr = original_root.get();
+
+  // Test that NOT iterator cannot pass predicate through, and the filter is
+  // applied at the top level.
+  SearchSpecProto search_spec;
+  search_spec.add_schema_type_filters(schema1_);
+  search_spec.add_namespace_filters(namespace1_);
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  // We should get a new tree:
+  //            Filter (root)
+  //                 |
+  //                NOT
+  //                 |
+  //         DummyHandlingFilter
+  std::unique_ptr<DocHitInfoIterator> new_root =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_root), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
+
+  // The filter should be applied at the top level, so the pointer should
+  // change and is type of DocHitInfoIteratorFilter.
+  EXPECT_THAT(new_root.get(), Ne(original_root_ptr));
+  ASSERT_THAT(new_root.get(),
+              WhenDynamicCastTo<DocHitInfoIteratorFilter*>(Ne(nullptr)));
+  // Check that the original iterator is now the delegate.
+  EXPECT_THAT(new_root->GetChildren(),
+              ElementsAre(Pointee(Pointer(original_root_ptr))));
+  // Check that the predicate is not applied to the original iterator.
+  EXPECT_THAT(iterator_handling_filter_ptr->document_filter_predicates(),
+              IsEmpty());
+}
+
+TEST_F(DocHitInfoIteratorFilterTest, ApplyFilterDeepTreeMixedHandling) {
+  // Create an iterator tree:
+  //                        AND (root)
+  //                         /    \
+  //       DummyHandlingFilter    AND
+  //                             /   \
+  //                           Dummy  DummyHandlingFilter
+  std::unique_ptr<DocHitInfoIteratorDummyHandlingFilter> child1 =
+      std::make_unique<DocHitInfoIteratorDummyHandlingFilter>();
+  std::unique_ptr<DocHitInfoIteratorDummy> grandchild1 =
+      std::make_unique<DocHitInfoIteratorDummy>();
+  std::unique_ptr<DocHitInfoIteratorDummyHandlingFilter> grandchild2 =
+      std::make_unique<DocHitInfoIteratorDummyHandlingFilter>();
+
+  DocHitInfoIteratorDummyHandlingFilter* child1_ptr = child1.get();
+  DocHitInfoIteratorDummyHandlingFilter* grandchild2_ptr = grandchild2.get();
+
+  std::unique_ptr<DocHitInfoIterator> child2 =
+      std::make_unique<DocHitInfoIteratorAnd>(std::move(grandchild1),
+                                              std::move(grandchild2));
+  std::unique_ptr<DocHitInfoIterator> original_root =
+      std::make_unique<DocHitInfoIteratorAnd>(std::move(child1),
+                                              std::move(child2));
+  DocHitInfoIterator* original_root_ptr = original_root.get();
+
+  // Create a predicate.
+  SearchSpecProto search_spec;
+  search_spec.add_schema_type_filters(schema1_);
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+
+  // Apply the filter. Since grandchild1 (Dummy) cannot handle the filter, a
+  // new filter iterator should be created at the top. However, child1 and
+  // grandchild2 should still get the predicate.
+  //
+  // We should get a new tree:
+  //                      Filter (root)
+  //                           |
+  //                          AND
+  //                         /    \
+  //       DummyHandlingFilter    AND
+  //                             /   \
+  //                           Dummy  DummyHandlingFilter
+  std::unique_ptr<DocHitInfoIterator> new_root =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_root), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
+
+  // The filter should be applied at the top level.
+  EXPECT_THAT(new_root.get(), Ne(original_root_ptr));
+  ASSERT_THAT(new_root.get(),
+              WhenDynamicCastTo<DocHitInfoIteratorFilter*>(Ne(nullptr)));
+  // Check that the original iterator is now the delegate.
+  EXPECT_THAT(new_root->GetChildren(),
+              ElementsAre(Pointee(Pointer(original_root_ptr))));
+  // Verify the predicate was passed down to the children that can handle it.
+  EXPECT_THAT(child1_ptr->document_filter_predicates(),
+              ElementsAre(predicate.get()));
+  EXPECT_THAT(grandchild2_ptr->document_filter_predicates(),
+              ElementsAre(predicate.get()));
+}
+
+TEST_F(DocHitInfoIteratorFilterTest, ApplyFilterNestedNonPassingIterator) {
+  // Create an iterator tree:
+  //                        AND (root)
+  //                         /    \
+  //       DummyHandlingFilter    NOT
+  //                               |
+  //                       DummyHandlingFilter
+  std::unique_ptr<DocHitInfoIteratorDummyHandlingFilter> child1 =
+      std::make_unique<DocHitInfoIteratorDummyHandlingFilter>();
+  std::unique_ptr<DocHitInfoIteratorDummyHandlingFilter> grandchild1 =
+      std::make_unique<DocHitInfoIteratorDummyHandlingFilter>();
+
+  DocHitInfoIteratorDummyHandlingFilter* child1_ptr = child1.get();
+  DocHitInfoIteratorDummyHandlingFilter* grandchild1_ptr = grandchild1.get();
+
+  std::unique_ptr<DocHitInfoIterator> child2 =
+      std::make_unique<DocHitInfoIteratorNot>(std::move(grandchild1),
+                                              /*document_id_limit=*/100);
+  std::unique_ptr<DocHitInfoIterator> original_root =
+      std::make_unique<DocHitInfoIteratorAnd>(std::move(child1),
+                                              std::move(child2));
+  DocHitInfoIterator* original_root_ptr = original_root.get();
+
+  // Create a predicate.
+  SearchSpecProto search_spec;
+  search_spec.add_namespace_filters(namespace1_);
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+
+  // Apply the filter. Since child2 (NOT) cannot pass the filter through, a
+  // new filter iterator should be created at the top. child1 should still get
+  // the predicate, but grandchild1 (under NOT) should not.
+  //
+  // We should get a new tree:
+  //                      Filter (root)
+  //                           |
+  //                          AND
+  //                         /    \
+  //       DummyHandlingFilter    NOT
+  //                               |
+  //                       DummyHandlingFilter
+  std::unique_ptr<DocHitInfoIterator> new_root =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_root), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
+
+  // The filter should be applied at the top level.
+  EXPECT_THAT(new_root.get(), Ne(original_root_ptr));
+  ASSERT_THAT(new_root.get(),
+              WhenDynamicCastTo<DocHitInfoIteratorFilter*>(Ne(nullptr)));
+  // Check that the original iterator is now the delegate.
+  EXPECT_THAT(new_root->GetChildren(),
+              ElementsAre(Pointee(Pointer(original_root_ptr))));
+  // Verify the predicate was passed down only to child1.
+  EXPECT_THAT(child1_ptr->document_filter_predicates(),
+              ElementsAre(predicate.get()));
+  // Verify the predicate was *not* passed down to grandchild1.
+  EXPECT_THAT(grandchild1_ptr->document_filter_predicates(), IsEmpty());
+}
+
+TEST_F(DocHitInfoIteratorFilterTest, ApplyFilterComplexTreeAllHandling) {
+  // Create leaf nodes that can handle filters.
+  auto leaf1 = std::make_unique<DocHitInfoIteratorDummyHandlingFilter>();
+  auto leaf2 = std::make_unique<DocHitInfoIteratorDummyHandlingFilter>();
+  auto leaf3 = std::make_unique<DocHitInfoIteratorDummyHandlingFilter>();
+  auto leaf4 = std::make_unique<DocHitInfoIteratorDummyHandlingFilter>();
+  DocHitInfoIteratorDummyHandlingFilter* leaf1_ptr = leaf1.get();
+  DocHitInfoIteratorDummyHandlingFilter* leaf2_ptr = leaf2.get();
+  DocHitInfoIteratorDummyHandlingFilter* leaf3_ptr = leaf3.get();
+  DocHitInfoIteratorDummyHandlingFilter* leaf4_ptr = leaf4.get();
+
+  // Build a complex tree: AND(OR(leaf1, leaf2), AND(leaf3, leaf4))
+  auto or_node = std::make_unique<DocHitInfoIteratorOr>(std::move(leaf1),
+                                                        std::move(leaf2));
+
+  auto and_node2 = std::make_unique<DocHitInfoIteratorAnd>(std::move(leaf3),
+                                                           std::move(leaf4));
+
+  std::unique_ptr<DocHitInfoIterator> original_root =
+      std::make_unique<DocHitInfoIteratorAnd>(std::move(or_node),
+                                              std::move(and_node2));
+  DocHitInfoIterator* original_root_ptr = original_root.get();
+
+  // Create a predicate.
+  SearchSpecProto search_spec;
+  search_spec.add_namespace_filters(namespace1_);
+  search_spec.add_schema_type_filters(schema1_);
+  std::unique_ptr<DocumentFilterPredicate> predicate =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+
+  // Apply the filter.
+  std::unique_ptr<DocHitInfoIterator> new_root =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_root), predicate.get(),
+          feature_flags_->enable_passing_filter_to_children());
+
+  // Since all leaf nodes can handle the filter, the original root should be
+  // returned without a Filter wrapper.
+  EXPECT_THAT(new_root.get(), Eq(original_root_ptr));
+  EXPECT_THAT(new_root.get(),
+              WhenDynamicCastTo<DocHitInfoIteratorFilter*>(nullptr));
+
+  // Verify the predicate was passed down to all leaf nodes.
+  EXPECT_THAT(leaf1_ptr->document_filter_predicates(),
+              ElementsAre(predicate.get()));
+  EXPECT_THAT(leaf2_ptr->document_filter_predicates(),
+              ElementsAre(predicate.get()));
+  EXPECT_THAT(leaf3_ptr->document_filter_predicates(),
+              ElementsAre(predicate.get()));
+  EXPECT_THAT(leaf4_ptr->document_filter_predicates(),
+              ElementsAre(predicate.get()));
+}
+
+TEST_F(DocHitInfoIteratorFilterTest, ApplyMultipleFiltersSequentially) {
+  // Create an iterator tree:
+  //                        AND (root)
+  //                         /    \
+  //       DummyHandlingFilter    Dummy
+  std::unique_ptr<DocHitInfoIteratorDummyHandlingFilter> child1 =
+      std::make_unique<DocHitInfoIteratorDummyHandlingFilter>();
+  std::unique_ptr<DocHitInfoIteratorDummy> child2 =
+      std::make_unique<DocHitInfoIteratorDummy>();
+  DocHitInfoIteratorDummyHandlingFilter* child1_ptr = child1.get();
+  std::unique_ptr<DocHitInfoIterator> original_root =
+      std::make_unique<DocHitInfoIteratorAnd>(std::move(child1),
+                                              std::move(child2));
+
+  // Create predicates.
+  SearchSpecProto search_spec_1;
+  search_spec_1.add_schema_type_filters(schema1_);
+  std::unique_ptr<DocumentFilterPredicate> predicate_1 =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec_1, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+  SearchSpecProto search_spec_2;
+  search_spec_2.add_namespace_filters(namespace1_);
+  std::unique_ptr<DocumentFilterPredicate> predicate_2 =
+      GetFilterPredicateBySchemaAndNamespace(
+          search_spec_2, *document_store_, *schema_store_,
+          fake_clock_.GetSystemTimeMilliseconds());
+
+  // Apply the first filter. Since child2 cannot handle it, a Filter iterator
+  // is added. child1 gets predicate_1.
+  // Tree: Filter1(AND(DummyHandlingFilter, Dummy))
+  std::unique_ptr<DocHitInfoIterator> current_root =
+      DocHitInfoIteratorFilter::ApplyFilter(
+          std::move(original_root), predicate_1.get(),
+          feature_flags_->enable_passing_filter_to_children());
+  DocHitInfoIterator* filter1_ptr = current_root.get();
+  ASSERT_THAT(filter1_ptr,
+              WhenDynamicCastTo<DocHitInfoIteratorFilter*>(Ne(nullptr)));
+  EXPECT_THAT(child1_ptr->document_filter_predicates(),
+              ElementsAre(predicate_1.get()));
+
+  // Apply the second filter. A new Filter2 iterator is added on top. child1
+  // gets predicate_2. Tree: Filter2(Filter1(AND(DummyHandlingFilter, Dummy)))
+  current_root = DocHitInfoIteratorFilter::ApplyFilter(
+      std::move(current_root), predicate_2.get(),
+      feature_flags_->enable_passing_filter_to_children());
+  DocHitInfoIterator* filter2_ptr = current_root.get();
+  ASSERT_THAT(filter2_ptr,
+              WhenDynamicCastTo<DocHitInfoIteratorFilter*>(Ne(nullptr)));
+  EXPECT_THAT(filter2_ptr, Ne(filter1_ptr));  // New filter added
+
+  // Check delegate of Filter2 is Filter1
+  ASSERT_THAT(current_root->GetChildren(),
+              ElementsAre(Pointee(Pointer(filter1_ptr))));
+
+  // Verify child1 now has both predicates.
+  EXPECT_THAT(child1_ptr->document_filter_predicates(),
+              ElementsAre(predicate_1.get(), predicate_2.get()));
+}
+
 }  // namespace
 
 }  // namespace lib
diff --git a/icing/index/iterator/doc-hit-info-iterator-match-score-expression.h b/icing/index/iterator/doc-hit-info-iterator-match-score-expression.h
index f46b8da..9c1ece6 100644
--- a/icing/index/iterator/doc-hit-info-iterator-match-score-expression.h
+++ b/icing/index/iterator/doc-hit-info-iterator-match-score-expression.h
@@ -33,7 +33,8 @@
 namespace icing {
 namespace lib {
 
-class DocHitInfoIteratorMatchScoreExpression : public DocHitInfoIterator {
+class DocHitInfoIteratorMatchScoreExpression
+    : public DocHitInfoIteratorSectionRestrictionNotApplicable {
  public:
   explicit DocHitInfoIteratorMatchScoreExpression(
       DocumentId last_added_document_id,
@@ -56,8 +57,8 @@
         "supported");
   }
 
-  void MapChildren(const ChildrenMapper& mapper) override {
-    delegate_ = mapper(std::move(delegate_));
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    return {&delegate_};
   }
 
   CallStats GetCallStats() const override { return delegate_->GetCallStats(); }
diff --git a/icing/index/iterator/doc-hit-info-iterator-none.h b/icing/index/iterator/doc-hit-info-iterator-none.h
index c2853f1..4d8bb05 100644
--- a/icing/index/iterator/doc-hit-info-iterator-none.h
+++ b/icing/index/iterator/doc-hit-info-iterator-none.h
@@ -15,10 +15,12 @@
 #ifndef ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_NONE_H_
 #define ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_NONE_H_
 
-#include <cstdint>
+#include <memory>
 #include <string>
+#include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
 
@@ -26,7 +28,8 @@
 namespace lib {
 
 // Iterator that will return no results.
-class DocHitInfoIteratorNone : public DocHitInfoIterator {
+class DocHitInfoIteratorNone
+    : public DocHitInfoIteratorSectionRestrictionNotApplicable {
  public:
   libtextclassifier3::Status Advance() override {
     return absl_ports::ResourceExhaustedError(
@@ -39,7 +42,9 @@
     return node;
   }
 
-  void MapChildren(const ChildrenMapper& mapper) override {}
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    return {};
+  }
 
   CallStats GetCallStats() const override { return CallStats(); }
 
diff --git a/icing/index/iterator/doc-hit-info-iterator-not.cc b/icing/index/iterator/doc-hit-info-iterator-not.cc
index 10a8292..706a5bd 100644
--- a/icing/index/iterator/doc-hit-info-iterator-not.cc
+++ b/icing/index/iterator/doc-hit-info-iterator-not.cc
@@ -14,11 +14,13 @@
 
 #include "icing/index/iterator/doc-hit-info-iterator-not.h"
 
-#include <cstdint>
 #include <memory>
+#include <string>
 #include <utility>
+#include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/absl_ports/str_cat.h"
 #include "icing/index/hit/doc-hit-info.h"
@@ -69,8 +71,9 @@
       "Cannot generate suggestion if the last term is NOT operator.");
 }
 
-void DocHitInfoIteratorNot::MapChildren(const ChildrenMapper& mapper) {
-  to_be_excluded_ = mapper(std::move(to_be_excluded_));
+std::vector<std::unique_ptr<DocHitInfoIterator>*>
+DocHitInfoIteratorNot::GetChildren() {
+  return {&to_be_excluded_};
 }
 
 std::string DocHitInfoIteratorNot::ToString() const {
diff --git a/icing/index/iterator/doc-hit-info-iterator-not.h b/icing/index/iterator/doc-hit-info-iterator-not.h
index 11575fb..2b02b37 100644
--- a/icing/index/iterator/doc-hit-info-iterator-not.h
+++ b/icing/index/iterator/doc-hit-info-iterator-not.h
@@ -15,11 +15,12 @@
 #ifndef ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_NOT_H_
 #define ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_NOT_H_
 
-#include <cstdint>
 #include <memory>
 #include <string>
+#include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/index/iterator/doc-hit-info-iterator-all-document-id.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
 #include "icing/store/document-id.h"
@@ -36,7 +37,8 @@
 // having been chosen because it's term was in a specific section. Since we
 // don't know anything about the sections for the Document, the
 // doc_hit_info.hit_section_ids_mask() is always kSectionIdMaskNone.
-class DocHitInfoIteratorNot : public DocHitInfoIterator {
+class DocHitInfoIteratorNot
+    : public DocHitInfoIteratorSectionRestrictionApplyToChildren {
  public:
   // to_be_excluded_iterator: The results of this iterator will be excluded
   //     from this iterator's results.
@@ -44,7 +46,7 @@
   //     Document to the DocumentStore
   explicit DocHitInfoIteratorNot(
       std::unique_ptr<DocHitInfoIterator> to_be_excluded_iterator,
-      const DocumentId document_id_limit);
+      DocumentId document_id_limit);
 
   libtextclassifier3::Status Advance() override;
 
@@ -53,7 +55,9 @@
   // to NOT operator.
   libtextclassifier3::StatusOr<TrimmedNode> TrimRightMostNode() && override;
 
-  void MapChildren(const ChildrenMapper& mapper) override;
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override;
+
+  bool CanPassFilterPredicateThrough() const override { return false; }
 
   CallStats GetCallStats() const override {
     return to_be_excluded_->GetCallStats() +
diff --git a/icing/index/iterator/doc-hit-info-iterator-not_test.cc b/icing/index/iterator/doc-hit-info-iterator-not_test.cc
index a8c835f..6b02cd3 100644
--- a/icing/index/iterator/doc-hit-info-iterator-not_test.cc
+++ b/icing/index/iterator/doc-hit-info-iterator-not_test.cc
@@ -108,7 +108,12 @@
       /*num_leaf_advance_calls_main_index_in=*/5,
       /*num_leaf_advance_calls_integer_index_in=*/3,
       /*num_leaf_advance_calls_no_index_in=*/1,
-      /*num_blocks_inspected_in=*/4);  // arbitrary value
+      /*num_blocks_inspected_in=*/4,
+      /*embedding_stats_in=*/
+      {.num_unquantized_embeddings_scored = 2,
+       .num_quantized_embeddings_scored = 3,
+       .unquantized_shards_read = {1, 2},
+       .quantized_shards_read{3, 4}});  // arbitrary value
   auto to_be_excluded_iterator = std::make_unique<DocHitInfoIteratorDummy>();
   to_be_excluded_iterator->SetCallStats(to_be_excluded_iterator_call_stats);
 
@@ -134,7 +139,8 @@
               .num_leaf_advance_calls_integer_index,
           to_be_excluded_iterator_call_stats.num_leaf_advance_calls_no_index +
               all_leaf_advance_calls,
-          to_be_excluded_iterator_call_stats.num_blocks_inspected));
+          to_be_excluded_iterator_call_stats.num_blocks_inspected,
+          to_be_excluded_iterator_call_stats.embedding_stats));
 }
 
 TEST(DocHitInfoIteratorNotTest, SectionIdsAlwaysNone) {
diff --git a/icing/index/iterator/doc-hit-info-iterator-or.h b/icing/index/iterator/doc-hit-info-iterator-or.h
index 8c0427b..c2da3eb 100644
--- a/icing/index/iterator/doc-hit-info-iterator-or.h
+++ b/icing/index/iterator/doc-hit-info-iterator-or.h
@@ -15,12 +15,16 @@
 #ifndef ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_OR_H_
 #define ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_OR_H_
 
-#include <cstdint>
+#include <cstddef>
 #include <memory>
 #include <string>
-#include <utility>
+#include <vector>
 
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/schema/section.h"
+#include "icing/store/document-id.h"
 
 namespace icing {
 namespace lib {
@@ -31,7 +35,8 @@
     std::vector<std::unique_ptr<DocHitInfoIterator>> iterators);
 
 // Iterate over a logical OR of two child iterators.
-class DocHitInfoIteratorOr : public DocHitInfoIterator {
+class DocHitInfoIteratorOr
+    : public DocHitInfoIteratorSectionRestrictionApplyToChildren {
  public:
   explicit DocHitInfoIteratorOr(std::unique_ptr<DocHitInfoIterator> left_it,
                                 std::unique_ptr<DocHitInfoIterator> right_it);
@@ -46,9 +51,8 @@
 
   std::string ToString() const override;
 
-  void MapChildren(const ChildrenMapper &mapper) override {
-    left_ = mapper(std::move(left_));
-    right_ = mapper(std::move(right_));
+  std::vector<std::unique_ptr<DocHitInfoIterator> *> GetChildren() override {
+    return {&left_, &right_};
   }
 
   void PopulateMatchedTermsStats(
@@ -81,7 +85,8 @@
 // Iterate over a logical OR of multiple child iterators.
 //
 // NOTE: DocHitInfoIteratorOr is a faster alternative to OR exactly 2 iterators.
-class DocHitInfoIteratorOrNary : public DocHitInfoIterator {
+class DocHitInfoIteratorOrNary
+    : public DocHitInfoIteratorSectionRestrictionApplyToChildren {
  public:
   explicit DocHitInfoIteratorOrNary(
       std::vector<std::unique_ptr<DocHitInfoIterator>> iterators);
@@ -94,10 +99,13 @@
 
   std::string ToString() const override;
 
-  void MapChildren(const ChildrenMapper &mapper) override {
+  std::vector<std::unique_ptr<DocHitInfoIterator> *> GetChildren() override {
+    std::vector<std::unique_ptr<DocHitInfoIterator> *> children;
+    children.reserve(iterators_.size());
     for (int i = 0; i < iterators_.size(); ++i) {
-      iterators_[i] = mapper(std::move(iterators_[i]));
+      children.push_back(&iterators_[i]);
     }
+    return children;
   }
 
   void PopulateMatchedTermsStats(
diff --git a/icing/index/iterator/doc-hit-info-iterator-or_test.cc b/icing/index/iterator/doc-hit-info-iterator-or_test.cc
index d198b53..2632b73 100644
--- a/icing/index/iterator/doc-hit-info-iterator-or_test.cc
+++ b/icing/index/iterator/doc-hit-info-iterator-or_test.cc
@@ -14,11 +14,16 @@
 
 #include "icing/index/iterator/doc-hit-info-iterator-or.h"
 
-#include <string>
+#include <memory>
+#include <unordered_map>
+#include <utility>
 #include <vector>
 
+#include "icing/text_classifier/lib3/utils/base/status.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "icing/index/hit/doc-hit-info.h"
+#include "icing/index/hit/hit.h"
 #include "icing/index/iterator/doc-hit-info-iterator-test-util.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
 #include "icing/schema/section.h"
@@ -33,6 +38,8 @@
 using ::testing::ElementsAre;
 using ::testing::Eq;
 using ::testing::IsEmpty;
+using ::testing::Pointee;
+using ::testing::Pointer;
 
 TEST(CreateAndIteratorTest, Or) {
   // Basic test that we can create a working Or iterator. Further testing of
@@ -83,7 +90,12 @@
       /*num_leaf_advance_calls_main_index_in=*/5,
       /*num_leaf_advance_calls_integer_index_in=*/3,
       /*num_leaf_advance_calls_no_index_in=*/1,
-      /*num_blocks_inspected_in=*/4);  // arbitrary value
+      /*num_blocks_inspected_in=*/4,
+      /*embedding_stats_in=*/
+      {.num_unquantized_embeddings_scored = 2,
+       .num_quantized_embeddings_scored = 3,
+       .unquantized_shards_read = {1, 2},
+       .quantized_shards_read{3, 4}});  // arbitrary value
   auto first_iter = std::make_unique<DocHitInfoIteratorDummy>();
   first_iter->SetCallStats(first_iter_call_stats);
 
@@ -92,7 +104,12 @@
       /*num_leaf_advance_calls_main_index_in=*/2,
       /*num_leaf_advance_calls_integer_index_in=*/10,
       /*num_leaf_advance_calls_no_index_in=*/3,
-      /*num_blocks_inspected_in=*/7);  // arbitrary value
+      /*num_blocks_inspected_in=*/7,
+      /*embedding_stats_in=*/
+      {.num_unquantized_embeddings_scored = 4,
+       .num_quantized_embeddings_scored = 5,
+       .unquantized_shards_read = {5, 6},
+       .quantized_shards_read{7}});  // arbitrary value
   auto second_iter = std::make_unique<DocHitInfoIteratorDummy>();
   second_iter->SetCallStats(second_iter_call_stats);
 
@@ -375,7 +392,12 @@
       /*num_leaf_advance_calls_main_index_in=*/5,
       /*num_leaf_advance_calls_integer_index_in=*/3,
       /*num_leaf_advance_calls_no_index_in=*/1,
-      /*num_blocks_inspected_in=*/4);  // arbitrary value
+      /*num_blocks_inspected_in=*/4,
+      /*embedding_stats_in=*/
+      {.num_unquantized_embeddings_scored = 2,
+       .num_quantized_embeddings_scored = 3,
+       .unquantized_shards_read = {1, 2},
+       .quantized_shards_read{3, 4}});  // arbitrary value
   auto first_iter = std::make_unique<DocHitInfoIteratorDummy>();
   first_iter->SetCallStats(first_iter_call_stats);
 
@@ -384,7 +406,12 @@
       /*num_leaf_advance_calls_main_index_in=*/2,
       /*num_leaf_advance_calls_integer_index_in=*/10,
       /*num_leaf_advance_calls_no_index_in=*/3,
-      /*num_blocks_inspected_in=*/7);  // arbitrary value
+      /*num_blocks_inspected_in=*/7,
+      /*embedding_stats_in=*/
+      {.num_unquantized_embeddings_scored = 4,
+       .num_quantized_embeddings_scored = 5,
+       .unquantized_shards_read = {5, 6},
+       .quantized_shards_read{7}});  // arbitrary value
   auto second_iter = std::make_unique<DocHitInfoIteratorDummy>();
   second_iter->SetCallStats(second_iter_call_stats);
 
@@ -393,7 +420,12 @@
       /*num_leaf_advance_calls_main_index_in=*/2000,
       /*num_leaf_advance_calls_integer_index_in=*/3000,
       /*num_leaf_advance_calls_no_index_in=*/0,
-      /*num_blocks_inspected_in=*/200);  // arbitrary value
+      /*num_blocks_inspected_in=*/200,
+      /*embedding_stats_in=*/
+      {.num_unquantized_embeddings_scored = 1,
+       .num_quantized_embeddings_scored = 1,
+       .unquantized_shards_read = {0},
+       .quantized_shards_read{0}});  // arbitrary value
   auto third_iter = std::make_unique<DocHitInfoIteratorDummy>();
   third_iter->SetCallStats(third_iter_call_stats);
 
@@ -402,7 +434,12 @@
       /*num_leaf_advance_calls_main_index_in=*/400,
       /*num_leaf_advance_calls_integer_index_in=*/100,
       /*num_leaf_advance_calls_no_index_in=*/20,
-      /*num_blocks_inspected_in=*/50);  // arbitrary value
+      /*num_blocks_inspected_in=*/50,
+      /*embedding_stats_in=*/
+      {.num_unquantized_embeddings_scored = 10,
+       .num_quantized_embeddings_scored = 10,
+       .unquantized_shards_read = {5, 6, 7},
+       .quantized_shards_read{9, 10, 11}});  // arbitrary value
   auto fourth_iter = std::make_unique<DocHitInfoIteratorDummy>();
   fourth_iter->SetCallStats(fourth_iter_call_stats);
 
@@ -578,6 +615,32 @@
   EXPECT_FALSE(or_iter.Advance().ok());
 }
 
+TEST(DocHitInfoIteratorOrNaryTest, GetChildren) {
+  std::vector<DocHitInfo> first_vector = {DocHitInfo(2), DocHitInfo(1),
+                                          DocHitInfo(0)};
+  std::vector<DocHitInfo> second_vector = {DocHitInfo(2), DocHitInfo(1)};
+  std::vector<DocHitInfo> third_vector = {DocHitInfo(2)};
+
+  std::vector<std::unique_ptr<DocHitInfoIterator>> iterators;
+  iterators.push_back(std::make_unique<DocHitInfoIteratorDummy>(first_vector));
+  iterators.push_back(std::make_unique<DocHitInfoIteratorDummy>(second_vector));
+  iterators.push_back(
+      std::make_unique<DocHitInfoIteratorDummy>(third_vector, "term", 10));
+
+  std::vector<DocHitInfoIterator*> iterator_ptrs;
+  for (const auto& iter : iterators) {
+    iterator_ptrs.push_back(iter.get());
+  }
+
+  std::unique_ptr<DocHitInfoIterator> iter =
+      std::make_unique<DocHitInfoIteratorOrNary>(std::move(iterators));
+
+  EXPECT_THAT(iter->GetChildren(),
+              ElementsAre(Pointee(Pointer(iterator_ptrs[0])),
+                          Pointee(Pointer(iterator_ptrs[1])),
+                          Pointee(Pointer(iterator_ptrs[2]))));
+}
+
 }  // namespace
 
 }  // namespace lib
diff --git a/icing/index/iterator/doc-hit-info-iterator-property-in-document.h b/icing/index/iterator/doc-hit-info-iterator-property-in-document.h
index bb2c97a..5a5724e 100644
--- a/icing/index/iterator/doc-hit-info-iterator-property-in-document.h
+++ b/icing/index/iterator/doc-hit-info-iterator-property-in-document.h
@@ -15,10 +15,8 @@
 #ifndef ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_PROPERTY_IN_DOCUMENT_H_
 #define ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_PROPERTY_IN_DOCUMENT_H_
 
-#include <cstdint>
 #include <memory>
 #include <string>
-#include <utility>
 #include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
@@ -34,10 +32,8 @@
 // post-processes metadata hits added by PropertyExistenceIndexingHandler.
 // Specifically, it filters out hits that are not recognized as metadata, and
 // always set hit_section_ids_mask to 0.
-//
-// It is marked as a subclass of DocHitInfoLeafIterator because section
-// restriction should not be passed down to meta_hit_iterator.
-class DocHitInfoIteratorPropertyInDocument : public DocHitInfoLeafIterator {
+class DocHitInfoIteratorPropertyInDocument
+    : public DocHitInfoIteratorSectionRestrictionNotApplicable {
  public:
   explicit DocHitInfoIteratorPropertyInDocument(
       std::unique_ptr<DocHitInfoIterator> meta_hit_iterator);
@@ -46,6 +42,10 @@
 
   libtextclassifier3::StatusOr<TrimmedNode> TrimRightMostNode() && override;
 
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    return {&meta_hit_iterator_};
+  }
+
   CallStats GetCallStats() const override {
     return meta_hit_iterator_->GetCallStats();
   }
diff --git a/icing/index/iterator/doc-hit-info-iterator-property-in-schema.h b/icing/index/iterator/doc-hit-info-iterator-property-in-schema.h
index d766712..5450594 100644
--- a/icing/index/iterator/doc-hit-info-iterator-property-in-schema.h
+++ b/icing/index/iterator/doc-hit-info-iterator-property-in-schema.h
@@ -19,7 +19,6 @@
 #include <memory>
 #include <set>
 #include <string>
-#include <utility>
 #include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
@@ -35,7 +34,8 @@
 
 // An iterator that helps filter for DocHitInfos whose schemas define the
 // properties named in target_properties_.
-class DocHitInfoIteratorPropertyInSchema : public DocHitInfoIterator {
+class DocHitInfoIteratorPropertyInSchema
+    : public DocHitInfoIteratorSectionRestrictionNotApplicable {
  public:
   // Does not take any ownership, and all pointers must refer to valid objects
   // that outlive the one constructed. The delegate should be at minimum be
@@ -50,8 +50,8 @@
 
   libtextclassifier3::StatusOr<TrimmedNode> TrimRightMostNode() && override;
 
-  void MapChildren(const ChildrenMapper& mapper) override {
-    delegate_ = mapper(std::move(delegate_));
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    return {&delegate_};
   }
 
   CallStats GetCallStats() const override { return delegate_->GetCallStats(); }
diff --git a/icing/index/iterator/doc-hit-info-iterator-property-in-schema_test.cc b/icing/index/iterator/doc-hit-info-iterator-property-in-schema_test.cc
index 3c29c1e..36c68af 100644
--- a/icing/index/iterator/doc-hit-info-iterator-property-in-schema_test.cc
+++ b/icing/index/iterator/doc-hit-info-iterator-property-in-schema_test.cc
@@ -19,6 +19,7 @@
 #include <utility>
 #include <vector>
 
+#include "icing/text_classifier/lib3/utils/base/status.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "icing/document-builder.h"
@@ -29,6 +30,7 @@
 #include "icing/index/iterator/doc-hit-info-iterator-all-document-id.h"
 #include "icing/index/iterator/doc-hit-info-iterator-test-util.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/schema-builder.h"
@@ -40,6 +42,7 @@
 #include "icing/testing/fake-clock.h"
 #include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/document-util.h"
 
 namespace icing {
 namespace lib {
@@ -100,14 +103,18 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, test_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, test_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
   }
 
@@ -134,8 +141,9 @@
 TEST_F(DocHitInfoIteratorPropertyInSchemaTest,
        AdvanceToDocumentWithIndexedProperty) {
   // Populate the DocumentStore's FilterCache with this document's data
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1_)));
   DocumentId document_id = put_result.new_document_id;
 
   auto original_iterator = std::make_unique<DocHitInfoIteratorAllDocumentId>(
@@ -155,8 +163,9 @@
 TEST_F(DocHitInfoIteratorPropertyInSchemaTest,
        AdvanceToDocumentWithUnindexedProperty) {
   // Populate the DocumentStore's FilterCache with this document's data
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1_)));
   DocumentId document_id = put_result.new_document_id;
 
   auto original_iterator = std::make_unique<DocHitInfoIteratorAllDocumentId>(
@@ -174,7 +183,8 @@
 }
 
 TEST_F(DocHitInfoIteratorPropertyInSchemaTest, NoMatchWithUndefinedProperty) {
-  ICING_EXPECT_OK(document_store_->Put(document1_));
+  ICING_EXPECT_OK(
+      document_store_->Put(document_util::CreateDocumentWrapper(document1_)));
 
   auto original_iterator = std::make_unique<DocHitInfoIteratorAllDocumentId>(
       document_store_->num_documents());
@@ -189,8 +199,9 @@
 TEST_F(DocHitInfoIteratorPropertyInSchemaTest,
        CorrectlySetsSectionIdMasksAndPopulatesTermMatchInfo) {
   // Populate the DocumentStore's FilterCache with this document's data
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1_)));
   DocumentId document_id = put_result.new_document_id;
 
   // Arbitrary section ids for the documents in the DocHitInfoIterators.
@@ -252,11 +263,13 @@
 
 TEST_F(DocHitInfoIteratorPropertyInSchemaTest,
        FindPropertyDefinedByMultipleTypes) {
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1_)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2_)));
   DocumentId document_id2 = put_result2.new_document_id;
   auto original_iterator = std::make_unique<DocHitInfoIteratorAllDocumentId>(
       document_store_->num_documents());
diff --git a/icing/index/iterator/doc-hit-info-iterator-section-restrict.cc b/icing/index/iterator/doc-hit-info-iterator-section-restrict.cc
index f6e440d..7726736 100644
--- a/icing/index/iterator/doc-hit-info-iterator-section-restrict.cc
+++ b/icing/index/iterator/doc-hit-info-iterator-section-restrict.cc
@@ -21,7 +21,6 @@
 #include <string_view>
 #include <unordered_map>
 #include <utility>
-#include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
@@ -29,12 +28,12 @@
 #include "icing/absl_ports/str_cat.h"
 #include "icing/absl_ports/str_join.h"
 #include "icing/index/hit/doc-hit-info.h"
+#include "icing/index/iterator/doc-hit-info-iterator-data-holder.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
 #include "icing/index/iterator/section-restrict-data.h"
 #include "icing/proto/search.pb.h"
 #include "icing/schema/schema-store.h"
 #include "icing/schema/section.h"
-#include "icing/store/document-filter-data.h"
 #include "icing/store/document-id.h"
 #include "icing/store/document-store.h"
 #include "icing/util/status-macros.h"
@@ -42,51 +41,6 @@
 namespace icing {
 namespace lib {
 
-// An iterator that simply takes ownership of SectionRestrictData.
-class SectionRestrictDataHolderIterator : public DocHitInfoIterator {
- public:
-  explicit SectionRestrictDataHolderIterator(
-      std::unique_ptr<DocHitInfoIterator> delegate,
-      std::unique_ptr<SectionRestrictData> data)
-      : delegate_(std::move(delegate)), data_(std::move(data)) {}
-
-  libtextclassifier3::Status Advance() override {
-    auto result = delegate_->Advance();
-    doc_hit_info_ = delegate_->doc_hit_info();
-    return result;
-  }
-
-  libtextclassifier3::StatusOr<TrimmedNode> TrimRightMostNode() && override {
-    ICING_ASSIGN_OR_RETURN(TrimmedNode trimmed_delegate,
-                           std::move(*delegate_).TrimRightMostNode());
-    if (trimmed_delegate.iterator_ != nullptr) {
-      trimmed_delegate.iterator_ =
-          std::make_unique<SectionRestrictDataHolderIterator>(
-              std::move(trimmed_delegate.iterator_), std::move(data_));
-    }
-    return trimmed_delegate;
-  }
-
-  void MapChildren(const ChildrenMapper& mapper) override {
-    delegate_ = mapper(std::move(delegate_));
-  }
-
-  CallStats GetCallStats() const override { return delegate_->GetCallStats(); }
-
-  std::string ToString() const override { return delegate_->ToString(); }
-
-  void PopulateMatchedTermsStats(
-      std::vector<TermMatchInfo>* matched_terms_stats,
-      SectionIdMask filtering_section_mask) const override {
-    return delegate_->PopulateMatchedTermsStats(matched_terms_stats,
-                                                filtering_section_mask);
-  }
-
- private:
-  std::unique_ptr<DocHitInfoIterator> delegate_;
-  std::unique_ptr<SectionRestrictData> data_;
-};
-
 DocHitInfoIteratorSectionRestrict::DocHitInfoIteratorSectionRestrict(
     std::unique_ptr<DocHitInfoIterator> delegate, SectionRestrictData* data)
     : delegate_(std::move(delegate)), data_(data) {}
@@ -103,8 +57,8 @@
       document_store, schema_store, current_time_ms, type_property_filters);
   std::unique_ptr<DocHitInfoIterator> result =
       ApplyRestrictions(std::move(iterator), data.get());
-  return std::make_unique<SectionRestrictDataHolderIterator>(std::move(result),
-                                                             std::move(data));
+  return std::make_unique<DocHitInfoIteratorDataHolder<SectionRestrictData>>(
+      std::move(result), std::move(data));
 }
 
 std::unique_ptr<DocHitInfoIterator>
@@ -124,27 +78,37 @@
       document_store, schema_store, current_time_ms, type_property_filters);
   std::unique_ptr<DocHitInfoIterator> result =
       ApplyRestrictions(std::move(iterator), data.get());
-  return std::make_unique<SectionRestrictDataHolderIterator>(std::move(result),
-                                                             std::move(data));
+  return std::make_unique<DocHitInfoIteratorDataHolder<SectionRestrictData>>(
+      std::move(result), std::move(data));
 }
 
 std::unique_ptr<DocHitInfoIterator>
 DocHitInfoIteratorSectionRestrict::ApplyRestrictions(
     std::unique_ptr<DocHitInfoIterator> iterator, SectionRestrictData* data) {
-  ChildrenMapper mapper;
-  mapper = [&data, &mapper](std::unique_ptr<DocHitInfoIterator> iterator)
-      -> std::unique_ptr<DocHitInfoIterator> {
-    if (iterator->HandleSectionRestriction(data)) {
-      return iterator;
-    } else if (iterator->is_leaf()) {
-      return std::make_unique<DocHitInfoIteratorSectionRestrict>(
-          std::move(iterator), data);
-    } else {
-      iterator->MapChildren(mapper);
-      return iterator;
-    }
-  };
-  return mapper(std::move(iterator));
+  // If the iterator does not respect section restrictions, just return it.
+  if (iterator->SectionRestrictionNotApplicable()) {
+    return iterator;
+  }
+
+  // If the iterator can internally handle the section restriction, apply it and
+  // return the iterator.
+  if (iterator->HandleSectionRestriction(data)) {
+    return iterator;
+  }
+
+  // If the iterator accepts section restriction, but does not want to pass it
+  // down to its children, return a new iterator with the section restriction
+  // applied at the top.
+  if (!iterator->SectionRestrictionShouldApplyToChildren()) {
+    return std::make_unique<DocHitInfoIteratorSectionRestrict>(
+        std::move(iterator), data);
+  }
+
+  // Otherwise, apply the section restriction to its children.
+  for (std::unique_ptr<DocHitInfoIterator>* child : iterator->GetChildren()) {
+    *child = ApplyRestrictions(std::move(*child), data);
+  }
+  return iterator;
 }
 
 libtextclassifier3::Status DocHitInfoIteratorSectionRestrict::Advance() {
diff --git a/icing/index/iterator/doc-hit-info-iterator-section-restrict.h b/icing/index/iterator/doc-hit-info-iterator-section-restrict.h
index 387ff52..d67c95a 100644
--- a/icing/index/iterator/doc-hit-info-iterator-section-restrict.h
+++ b/icing/index/iterator/doc-hit-info-iterator-section-restrict.h
@@ -41,18 +41,18 @@
 // That class is meant to be applied to the root of a query tree and filter over
 // all results at the end. This class is more used in the limited scope of a
 // term or a small group of terms.
-class DocHitInfoIteratorSectionRestrict : public DocHitInfoLeafIterator {
+class DocHitInfoIteratorSectionRestrict : public DocHitInfoIterator {
  public:
   // Does not take any ownership, and all pointers must refer to valid objects
   // that outlive the one constructed.
   explicit DocHitInfoIteratorSectionRestrict(
       std::unique_ptr<DocHitInfoIterator> delegate, SectionRestrictData* data);
 
-  // Methods that apply section restrictions to all DocHitInfoLeafIterator nodes
-  // inside the provided iterator tree, and return the root of the tree
-  // afterwards. These methods do not take any ownership for the raw pointer
-  // parameters, which must refer to valid objects that outlive the iterator
-  // returned.
+  // Methods that apply section restrictions to all applicable nodes at the
+  // lowest level inside the provided iterator tree, and return the root of the
+  // tree afterwards. These methods do not take any ownership for the raw
+  // pointer parameters, which must refer to valid objects that outlive the
+  // iterator returned.
   static std::unique_ptr<DocHitInfoIterator> ApplyRestrictions(
       std::unique_ptr<DocHitInfoIterator> iterator,
       const DocumentStore* document_store, const SchemaStore* schema_store,
@@ -72,9 +72,9 @@
 
   std::string ToString() const override;
 
-  // Note that the DocHitInfoIteratorSectionRestrict can only be applied at
-  // DocHitInfoLeafIterator, which can be a term iterator or another
-  // DocHitInfoIteratorSectionRestrict.
+  // Note that the DocHitInfoIteratorSectionRestrict will eventually be applied
+  // at the lowest level of the iterator tree, which can be a term iterator or
+  // another DocHitInfoIteratorSectionRestrict.
   //
   // To filter the matching sections, filtering_section_mask should be set to
   // doc_hit_info_.hit_section_ids_mask() held in the outermost
@@ -96,6 +96,10 @@
             doc_hit_info_.hit_section_ids_mask());
   }
 
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    return {&delegate_};
+  }
+
  private:
   std::unique_ptr<DocHitInfoIterator> delegate_;
   // Does not own.
diff --git a/icing/index/iterator/doc-hit-info-iterator-section-restrict_test.cc b/icing/index/iterator/doc-hit-info-iterator-section-restrict_test.cc
index a97f2a8..1c414c1 100644
--- a/icing/index/iterator/doc-hit-info-iterator-section-restrict_test.cc
+++ b/icing/index/iterator/doc-hit-info-iterator-section-restrict_test.cc
@@ -17,6 +17,7 @@
 #include <memory>
 #include <set>
 #include <string>
+#include <unordered_map>
 #include <utility>
 #include <vector>
 
@@ -27,9 +28,11 @@
 #include "icing/file/filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/index/hit/doc-hit-info.h"
+#include "icing/index/hit/hit.h"
 #include "icing/index/iterator/doc-hit-info-iterator-and.h"
 #include "icing/index/iterator/doc-hit-info-iterator-test-util.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/proto/term.pb.h"
@@ -42,6 +45,7 @@
 #include "icing/testing/fake-clock.h"
 #include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/document-util.h"
 
 namespace icing {
 namespace lib {
@@ -105,14 +109,18 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, test_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, test_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
   }
 
@@ -139,8 +147,9 @@
 TEST_F(DocHitInfoIteratorSectionRestrictTest,
        PopulateMatchedTermsStats_IncludesHitWithMatchingSection) {
   // Populate the DocumentStore's FilterCache with this document's data
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1_)));
   DocumentId document_id = put_result.new_document_id;
 
   // Arbitrary section ids for the documents in the DocHitInfoIterators.
@@ -205,8 +214,9 @@
 
 TEST_F(DocHitInfoIteratorSectionRestrictTest, IncludesHitWithMatchingSection) {
   // Populate the DocumentStore's FilterCache with this document's data
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1_)));
   DocumentId document_id = put_result.new_document_id;
 
   SectionIdMask section_id_mask = 1U << kIndexedSectionId0;
@@ -233,8 +243,9 @@
 TEST_F(DocHitInfoIteratorSectionRestrictTest,
        IncludesHitWithMultipleMatchingSectionsWithMultipleSectionRestricts) {
   // Populate the DocumentStore's FilterCache with this document's data
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1_)));
   DocumentId document_id = put_result.new_document_id;
 
   SectionIdMask section_id_mask = 1U << kIndexedSectionId0;
@@ -265,8 +276,9 @@
 TEST_F(DocHitInfoIteratorSectionRestrictTest,
        IncludesHitWithMultipleMatchingSectionsWithSingleSectionRestrict) {
   // Populate the DocumentStore's FilterCache with this document's data
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1_)));
   DocumentId document_id = put_result.new_document_id;
 
   SectionIdMask section_id_mask = 1U << kIndexedSectionId0;
@@ -296,8 +308,9 @@
 TEST_F(DocHitInfoIteratorSectionRestrictTest,
        IncludesHitWithSingleMatchingSectionsWithMultiSectionRestrict) {
   // Populate the DocumentStore's FilterCache with this document's data
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1_)));
   DocumentId document_id = put_result.new_document_id;
 
   SectionIdMask section_id_mask = 1U << kIndexedSectionId1;
@@ -346,8 +359,9 @@
 TEST_F(DocHitInfoIteratorSectionRestrictTest,
        DoesntIncludeHitWithWrongSectionName) {
   // Populate the DocumentStore's FilterCache with this document's data
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1_)));
   DocumentId document_id = put_result.new_document_id;
 
   SectionIdMask section_id_mask = 1U << kIndexedSectionId0;
@@ -376,8 +390,9 @@
 TEST_F(DocHitInfoIteratorSectionRestrictTest,
        DoesntIncludeHitWithNoSectionIds) {
   // Populate the DocumentStore's FilterCache with this document's data
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1_)));
   DocumentId document_id = put_result.new_document_id;
 
   // Create a hit that doesn't exist in any sections, so it shouldn't match any
@@ -404,8 +419,9 @@
 TEST_F(DocHitInfoIteratorSectionRestrictTest,
        DoesntIncludeHitWithDifferentSectionId) {
   // Populate the DocumentStore's FilterCache with this document's data
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1_)));
   DocumentId document_id = put_result.new_document_id;
 
   // Anything that's not 0, which is the indexed property
@@ -438,7 +454,12 @@
       /*num_leaf_advance_calls_main_index_in=*/5,
       /*num_leaf_advance_calls_integer_index_in=*/3,
       /*num_leaf_advance_calls_no_index_in=*/1,
-      /*num_blocks_inspected_in=*/4);  // arbitrary value
+      /*num_blocks_inspected_in=*/4,
+      /*embedding_stats_in=*/
+      {.num_unquantized_embeddings_scored = 2,
+       .num_quantized_embeddings_scored = 3,
+       .unquantized_shards_read = {1, 2},
+       .quantized_shards_read{3, 4}});  // arbitrary value
   auto original_iterator = std::make_unique<DocHitInfoIteratorDummy>();
   original_iterator->SetCallStats(original_call_stats);
 
@@ -455,14 +476,17 @@
 TEST_F(DocHitInfoIteratorSectionRestrictTest,
        TrimSectionRestrictIterator_TwoLayer) {
   // Populate the DocumentStore's FilterCache with this document's data
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1_)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2_)));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store_->Put(document3_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store_->Put(document_util::CreateDocumentWrapper(document3_)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   // 0 is the indexed property
@@ -516,11 +540,13 @@
 
 TEST_F(DocHitInfoIteratorSectionRestrictTest, TrimSectionRestrictIterator) {
   // Populate the DocumentStore's FilterCache with this document's data
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1_)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2_)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // 0 is the indexed property
diff --git a/icing/index/iterator/doc-hit-info-iterator-test-util.h b/icing/index/iterator/doc-hit-info-iterator-test-util.h
index cb6258d..78822c5 100644
--- a/icing/index/iterator/doc-hit-info-iterator-test-util.h
+++ b/icing/index/iterator/doc-hit-info-iterator-test-util.h
@@ -19,6 +19,7 @@
 #include <cinttypes>
 #include <cstdint>
 #include <cstring>
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
@@ -84,7 +85,7 @@
 // will then proceed to return the doc_hit_infos in order as Advance's are
 // called. After all doc_hit_infos are returned, Advance will return a NotFound
 // error (also like normal DocHitInfoIterators).
-class DocHitInfoIteratorDummy : public DocHitInfoLeafIterator {
+class DocHitInfoIteratorDummy : public DocHitInfoIterator {
  public:
   DocHitInfoIteratorDummy() = default;
   explicit DocHitInfoIteratorDummy(
@@ -121,6 +122,10 @@
     return node;
   }
 
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    return {};
+  }
+
   // Imitates behavior of DocHitInfoIteratorTermMain/DocHitInfoIteratorTermLite
   void PopulateMatchedTermsStats(
       std::vector<TermMatchInfo>* matched_terms_stats,
diff --git a/icing/index/iterator/doc-hit-info-iterator.h b/icing/index/iterator/doc-hit-info-iterator.h
index f4f15bb..9a85403 100644
--- a/icing/index/iterator/doc-hit-info-iterator.h
+++ b/icing/index/iterator/doc-hit-info-iterator.h
@@ -21,6 +21,7 @@
 #include <memory>
 #include <string>
 #include <string_view>
+#include <unordered_set>
 #include <utility>
 #include <vector>
 
@@ -36,6 +37,7 @@
 namespace lib {
 
 class SectionRestrictData;
+class DocumentFilterPredicate;
 
 // Data structure that maps a single matched query term to its section mask
 // and the list of term frequencies.
@@ -116,18 +118,60 @@
     // - Internal nodes: should aggregate values from all children.
     int32_t num_blocks_inspected;
 
+    // Stats related to embedding index scoring.
+    struct EmbeddingStats {
+      // The number of unquantized embeddings that have been scored.
+      int32_t num_unquantized_embeddings_scored = 0;
+      // The number of quantized embeddings that have been scored.
+      int32_t num_quantized_embeddings_scored = 0;
+      // The set of shards that have been read for unquantized embeddings.
+      std::unordered_set<uint32_t> unquantized_shards_read;
+      // The set of shards that have been read for quantized embeddings.
+      std::unordered_set<uint32_t> quantized_shards_read;
+      // The number of raw embedding bytes read.
+      int64_t num_embedding_bytes_read = 0;
+
+      bool operator==(const EmbeddingStats& other) const {
+        return num_unquantized_embeddings_scored ==
+                   other.num_unquantized_embeddings_scored &&
+               num_quantized_embeddings_scored ==
+                   other.num_quantized_embeddings_scored &&
+               unquantized_shards_read == other.unquantized_shards_read &&
+               quantized_shards_read == other.quantized_shards_read &&
+               num_embedding_bytes_read == other.num_embedding_bytes_read;
+      }
+
+      EmbeddingStats operator+(const EmbeddingStats& other) const {
+        EmbeddingStats result = *this;
+        result.num_unquantized_embeddings_scored +=
+            other.num_unquantized_embeddings_scored;
+        result.num_quantized_embeddings_scored +=
+            other.num_quantized_embeddings_scored;
+        result.unquantized_shards_read.insert(
+            other.unquantized_shards_read.begin(),
+            other.unquantized_shards_read.end());
+        result.quantized_shards_read.insert(other.quantized_shards_read.begin(),
+                                            other.quantized_shards_read.end());
+        result.num_embedding_bytes_read += other.num_embedding_bytes_read;
+        return result;
+      }
+    };
+    EmbeddingStats embedding_stats;
+
     explicit CallStats()
         : CallStats(/*num_leaf_advance_calls_lite_index_in=*/0,
                     /*num_leaf_advance_calls_main_index_in=*/0,
                     /*num_leaf_advance_calls_integer_index_in=*/0,
                     /*num_leaf_advance_calls_no_index_in=*/0,
-                    /*num_blocks_inspected_in=*/0) {}
+                    /*num_blocks_inspected_in=*/0,
+                    /*embedding_stats_in=*/{}) {}
 
     explicit CallStats(int32_t num_leaf_advance_calls_lite_index_in,
                        int32_t num_leaf_advance_calls_main_index_in,
                        int32_t num_leaf_advance_calls_integer_index_in,
                        int32_t num_leaf_advance_calls_no_index_in,
-                       int32_t num_blocks_inspected_in)
+                       int32_t num_blocks_inspected_in,
+                       EmbeddingStats embedding_stats_in)
         : num_leaf_advance_calls_lite_index(
               num_leaf_advance_calls_lite_index_in),
           num_leaf_advance_calls_main_index(
@@ -135,7 +179,8 @@
           num_leaf_advance_calls_integer_index(
               num_leaf_advance_calls_integer_index_in),
           num_leaf_advance_calls_no_index(num_leaf_advance_calls_no_index_in),
-          num_blocks_inspected(num_blocks_inspected_in) {}
+          num_blocks_inspected(num_blocks_inspected_in),
+          embedding_stats(std::move(embedding_stats_in)) {}
 
     int32_t num_leaf_advance_calls() const {
       return num_leaf_advance_calls_lite_index +
@@ -153,7 +198,8 @@
                  other.num_leaf_advance_calls_integer_index &&
              num_leaf_advance_calls_no_index ==
                  other.num_leaf_advance_calls_no_index &&
-             num_blocks_inspected == other.num_blocks_inspected;
+             num_blocks_inspected == other.num_blocks_inspected &&
+             embedding_stats == other.embedding_stats;
     }
 
     CallStats operator+(const CallStats& other) const {
@@ -165,7 +211,8 @@
                            other.num_leaf_advance_calls_integer_index,
                        num_leaf_advance_calls_no_index +
                            other.num_leaf_advance_calls_no_index,
-                       num_blocks_inspected + other.num_blocks_inspected);
+                       num_blocks_inspected + other.num_blocks_inspected,
+                       embedding_stats + other.embedding_stats);
     }
 
     CallStats& operator+=(const CallStats& other) {
@@ -212,21 +259,94 @@
   //   INVALID_ARGUMENT if the right-most node is not suppose to be trimmed.
   virtual libtextclassifier3::StatusOr<TrimmedNode> TrimRightMostNode() && = 0;
 
-  // Map all direct children of this iterator according to the passed mapper.
-  virtual void MapChildren(const ChildrenMapper& mapper) = 0;
+  // Returns raw pointers to the direct children of this iterator. Empty if this
+  // iterator has no children.
+  //
+  // This allows modifying the iterator tree structure, for example, by
+  // modifying the child iterators directly or even replacing them with new
+  // ones. The lifetime of the returned raw pointers is tied to this iterator
+  // object.
+  virtual std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() = 0;
 
-  virtual bool is_leaf() { return false; }
+  // Returns true if section restrictions are not applicable to this iterator.
+  //
+  // Several iterators do **not** need to respect section restrictions, since it
+  // does not have any section information. For example:
+  // - DocHitInfoIteratorAllDocumentId
+  // - DocHitInfoIteratorByUri
+  // - DocHitInfoIteratorMatchScoreExpression
+  // - DocHitInfoIteratorPropertyInSchema
+  // - DocHitInfoIteratorPropertyInDocument
+  //
+  // Unless DocHitInfoIteratorSectionRestrictionNotApplicable is extended, let's
+  // assume the iterator should respect section restrictions.
+  virtual bool SectionRestrictionNotApplicable() const { return false; }
+
+  // If not SectionRestrictionNotApplicable(), whether section restrictions
+  // should be passed down to the children iterators.
+  //
+  // Several iterators need to pass down section restrictions to their
+  // children to maintain the correct semantics of section restrictions. Check
+  // go/icing-section-restrict-fix for more details. For example:
+  // - DocHitInfoIteratorAnd
+  // - DocHitInfoIteratorOr
+  // - DocHitInfoIteratorFilter
+  //
+  // However, several iterators do respect section restrictions, but do not need
+  // to or cannot pass down section restrictions to their children. For example:
+  // - DocHitInfoIteratorSectionRestrict, since it's a section restriction
+  //   iterator itself. A new section restriction should be chained, instead of
+  //   passing down.
+  // - DocHitInfoIteratorTermLite, since it does not have any children. Section
+  //   restriction should be applied at the top of this iterator directly.
+  // - DocHitInfoIteratorEmbedding, since it does not have any children, and
+  //   in addition, it can internally handle the section restriction logic.
+  //
+  // Unless DocHitInfoIteratorSectionRestrictionApplyToChildren is extended,
+  // let's assume this is false, which means section restrictions should be
+  // applied at the top of this iterator directly or handled internally.
+  virtual bool SectionRestrictionShouldApplyToChildren() const { return false; }
 
   // Try to internally handle the provided section restriction in the iterator.
   //
   // Returns:
   //   - false if the iterator does not support handling section restriction.
   //   - true if the iterator supports handling section restriction, and the
-  //     section restriction has been applied.
+  //     section restriction has been applied. For example,
+  //     DocHitInfoIteratorEmbedding can internally handle the section
+  //     restriction logic.
   virtual bool HandleSectionRestriction(SectionRestrictData* other_data) {
     return false;
   }
 
+  // Whether a filter predicate can be passed through this iterator.
+  //
+  // Currently all iterators except for DocHitInfoIteratorNot are able to pass
+  // filter predicates through, while maintaining the same semantics.
+  virtual bool CanPassFilterPredicateThrough() const { return true; }
+
+  // Try to internally handle the provided filter in the iterator.
+  //
+  // Returns:
+  //   - false if the iterator does not support handling filter.
+  //   - true if the iterator supports handling filter, and the filter has been
+  //     applied.
+  virtual bool HandleFilter(const DocumentFilterPredicate* predicate) {
+    return false;
+  }
+
+  // Whether this iterator can adopt a delegate iterator.
+  //
+  // If true, then AdoptDelegate can be called to adopt a delegate iterator.
+  virtual bool CanAdoptDelegate() const { return false; }
+
+  // If CanAdoptDelegate returns false, then this method will have no effect.
+  //
+  // This iterator instance will then filter all of its hits to only include
+  // documents that are returned by the delegate iterator.
+  virtual void AdoptDelegate(std::unique_ptr<DocHitInfoIterator> delegate,
+                     bool delegate_node_is_right_most) {}
+
   virtual ~DocHitInfoIterator() = default;
 
   // Returns:
@@ -279,14 +399,16 @@
   }
 };
 
-// A leaf node is a term node or a chain of section restriction node applied on
-// a term node.
-class DocHitInfoLeafIterator : public DocHitInfoIterator {
+class DocHitInfoIteratorSectionRestrictionNotApplicable
+    : public DocHitInfoIterator {
  public:
-  bool is_leaf() override { return true; }
+  bool SectionRestrictionNotApplicable() const override { return true; }
+};
 
-  // Calling MapChildren on leaf node does not make sense, and will do nothing.
-  void MapChildren(const ChildrenMapper& mapper) override {}
+class DocHitInfoIteratorSectionRestrictionApplyToChildren
+    : public DocHitInfoIterator {
+ public:
+  bool SectionRestrictionShouldApplyToChildren() const override { return true; }
 };
 
 }  // namespace lib
diff --git a/icing/index/iterator/document-filter-predicate.h b/icing/index/iterator/document-filter-predicate.h
new file mode 100644
index 0000000..59e0d05
--- /dev/null
+++ b/icing/index/iterator/document-filter-predicate.h
@@ -0,0 +1,71 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef ICING_INDEX_ITERATOR_DOCUMENT_FILTER_PREDICATE_H_
+#define ICING_INDEX_ITERATOR_DOCUMENT_FILTER_PREDICATE_H_
+
+#include <vector>
+
+#include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/store/document-id.h"
+
+namespace icing {
+namespace lib {
+
+// An interface for a predicate that determines whether a document, identified
+// by its DocumentId, should be included in a result set.
+class DocumentFilterPredicate {
+ public:
+  virtual ~DocumentFilterPredicate() = default;
+
+  // Evaluates whether the given document_id satisfies the predicate. Returns
+  // true if the document satisfies the predicate and should be included, false
+  // otherwise.
+  virtual bool operator()(DocumentId document_id) const = 0;
+};
+
+// Indicate that the iterator can internally handle filtering logic by itself.
+//
+// This is helpful when some iterators want to have better control for
+// optimization. For example, embedding iterator will be able to filter out
+// embedding hits from unwanted documents to avoid retrieving unnecessary
+// vectors and calculate scores for them.
+class DocHitInfoIteratorHandlingFilter : virtual public DocHitInfoIterator {
+ protected:
+  // After accepting a filter predicate, the iterator will behave equivalently
+  // as if we had applied a filter with this predicate at the top of the
+  // iterator.
+  bool HandleFilter(const DocumentFilterPredicate* predicate) override {
+    document_filter_predicates_.push_back(predicate);
+    return true;
+  }
+
+  bool DoesDocumentPassAllFilters(DocumentId document_id) const {
+    for (const DocumentFilterPredicate* predicate :
+         document_filter_predicates_) {
+      if (!(*predicate)(document_id)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  // Does not own the pointers.
+  std::vector<const DocumentFilterPredicate*> document_filter_predicates_;
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_INDEX_ITERATOR_DOCUMENT_FILTER_PREDICATE_H_
diff --git a/icing/index/iterator/section-restrict-data.h b/icing/index/iterator/section-restrict-data.h
index 38d7158..208d629 100644
--- a/icing/index/iterator/section-restrict-data.h
+++ b/icing/index/iterator/section-restrict-data.h
@@ -114,7 +114,7 @@
 // embedding hits from unwanted sections to avoid retrieving unnecessary vectors
 // and calculate scores for them.
 class DocHitInfoIteratorHandlingSectionRestrict
-    : public DocHitInfoLeafIterator {
+    : virtual public DocHitInfoIterator {
  protected:
   bool HandleSectionRestriction(SectionRestrictData* other_data) override {
     section_restrict_data_.push_back(other_data);
diff --git a/icing/index/lite/doc-hit-info-iterator-term-lite.cc b/icing/index/lite/doc-hit-info-iterator-term-lite.cc
index c356203..c45cb7a 100644
--- a/icing/index/lite/doc-hit-info-iterator-term-lite.cc
+++ b/icing/index/lite/doc-hit-info-iterator-term-lite.cc
@@ -16,10 +16,8 @@
 
 #include <algorithm>
 #include <cstdint>
-#include <cstring>
 #include <numeric>
 #include <string>
-#include <utility>
 #include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
@@ -33,6 +31,7 @@
 #include "icing/index/term-id-codec.h"
 #include "icing/schema/section.h"
 #include "icing/util/logging.h"
+#include "icing/util/math-util.h"
 #include "icing/util/status-macros.h"
 
 namespace icing {
@@ -153,31 +152,9 @@
     // Now indices is a map from sorted index to current index. In other words,
     // the sorted cached_hits_[i] should be the current cached_hits_[indices[i]]
     // for every valid i.
-    std::vector<bool> done(indices.size());
-    // Apply permutation
-    for (int i = 0; i < indices.size(); ++i) {
-      if (done[i]) {
-        continue;
-      }
-      done[i] = true;
-      int curr = i;
-      int next = indices[i];
-      // Since every finite permutation is formed by disjoint cycles, we can
-      // start with the current element, at index i, and swap the element at
-      // this position with whatever element that *should* be here. Then,
-      // continue to swap the original element, at its updated positions, with
-      // the element that should be occupying that position until the original
-      // element has reached *its* correct position. This completes applying the
-      // single cycle in the permutation.
-      while (next != i) {
-        std::swap(cached_hits_[curr], cached_hits_[next]);
-        std::swap(cached_hit_term_frequency_[curr],
-                  cached_hit_term_frequency_[next]);
-        done[next] = true;
-        curr = next;
-        next = indices[next];
-      }
-    }
+
+    math_util::ApplyPermutation(indices, cached_hits_,
+                                cached_hit_term_frequency_);
   }
 }
 
diff --git a/icing/index/lite/doc-hit-info-iterator-term-lite.h b/icing/index/lite/doc-hit-info-iterator-term-lite.h
index a63d050..ff286d3 100644
--- a/icing/index/lite/doc-hit-info-iterator-term-lite.h
+++ b/icing/index/lite/doc-hit-info-iterator-term-lite.h
@@ -16,6 +16,7 @@
 #define ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_TERM_LITE_H_
 
 #include <array>
+#include <memory>
 #include <string>
 #include <utility>
 #include <vector>
@@ -32,7 +33,7 @@
 namespace icing {
 namespace lib {
 
-class DocHitInfoIteratorTermLite : public DocHitInfoLeafIterator {
+class DocHitInfoIteratorTermLite : public DocHitInfoIterator {
  public:
   explicit DocHitInfoIteratorTermLite(const TermIdCodec* term_id_codec,
                                       LiteIndex* lite_index,
@@ -55,13 +56,18 @@
 
   libtextclassifier3::StatusOr<TrimmedNode> TrimRightMostNode() && override;
 
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    return {};
+  }
+
   CallStats GetCallStats() const override {
     return CallStats(
         /*num_leaf_advance_calls_lite_index_in=*/num_advance_calls_,
         /*num_leaf_advance_calls_main_index_in=*/0,
         /*num_leaf_advance_calls_integer_index_in=*/0,
         /*num_leaf_advance_calls_no_index_in=*/0,
-        /*num_blocks_inspected_in=*/0);
+        /*num_blocks_inspected_in=*/0,
+        /*embedding_stats_in=*/{});
   }
 
   void PopulateMatchedTermsStats(
diff --git a/icing/index/lite/lite-index-header.h b/icing/index/lite/lite-index-header.h
index 1aba130..1677d69 100644
--- a/icing/index/lite/lite-index-header.h
+++ b/icing/index/lite/lite-index-header.h
@@ -22,6 +22,7 @@
 #include "icing/legacy/core/icing-string-util.h"
 #include "icing/store/document-id.h"
 #include "icing/util/crc32.h"
+#include "icing/util/logging.h"
 
 namespace icing {
 namespace lib {
@@ -55,7 +56,7 @@
 class LiteIndex_HeaderImpl : public LiteIndex_Header {
  public:
   struct HeaderData {
-    static const uint32_t kMagic = 0xC2EAD682;
+    static constexpr uint32_t kMagic = 0xC2EAD682;
 
     uint32_t lite_index_crc;
     uint32_t magic;
@@ -74,6 +75,10 @@
   explicit LiteIndex_HeaderImpl(HeaderData *hdr) : hdr_(hdr) {}
 
   bool check_magic() const override {
+    if (hdr_->magic != HeaderData::kMagic) {
+      ICING_LOG(ERROR) << "Invalid header magic for LiteIndex. Expected: "
+                       << HeaderData::kMagic << ", actual: " << hdr_->magic;
+    }
     return hdr_->magic == HeaderData::kMagic;
   }
 
diff --git a/icing/index/lite/lite-index.cc b/icing/index/lite/lite-index.cc
index 3862206..ee7b51f 100644
--- a/icing/index/lite/lite-index.cc
+++ b/icing/index/lite/lite-index.cc
@@ -195,7 +195,10 @@
 
     // Check integrity.
     if (!header_->check_magic()) {
-      status = absl_ports::InternalError("Lite index header magic mismatch");
+      ICING_LOG(ERROR) << "Invalid header magic for LiteIndex "
+                       << options_.filename_base;
+      status = absl_ports::InternalError(absl_ports::StrCat(
+          "Invalid header magic for LiteIndex: ", options_.filename_base));
       goto error;
     }
     Crc32 expected_crc(header_->lite_index_crc());
@@ -353,12 +356,9 @@
 
   TermIdHitPair term_id_hit_pair(term_id, hit);
   uint32_t cur_size = header_->cur_size();
-  TermIdHitPair::Value* valp =
-      hit_buffer_.GetMutableMem<TermIdHitPair::Value>(cur_size, 1);
-  if (valp == nullptr) {
-    return absl_ports::ResourceExhaustedError(
-        "Allocating more space in hit buffer failed!");
-  }
+  ICING_ASSIGN_OR_RETURN(
+      TermIdHitPair::Value * valp,
+      hit_buffer_.GetMutableMem<TermIdHitPair::Value>(cur_size, 1));
   *valp = term_id_hit_pair.value();
   header_->set_cur_size(cur_size + 1);
 
@@ -473,7 +473,12 @@
     // after need_sort_at_querying is evaluated.
     // We check need_sort_at_querying to improve query concurrency as threads
     // can avoid acquiring the unique lock if no sorting is needed.
-    SortHitsImpl();
+    libtextclassifier3::Status status = SortHitsImpl();
+    if (!status.ok()) {
+      // Log this error and continue.
+      ICING_LOG(ERROR) << "Failed to sort HitBuffer: "
+                       << status.error_message();
+    }
 
     if (options_.hit_buffer_sort_at_indexing) {
       // This is the second case for sort. Log as this should be a very rare
@@ -624,16 +629,26 @@
   return storage_info;
 }
 
-void LiteIndex::SortHitsImpl() {
+libtextclassifier3::Status LiteIndex::SortHitsImpl() {
   // Make searchable by sorting by hit buffer.
   uint32_t need_sort_len = GetHitBufferUnsortedSizeImpl();
   if (need_sort_len <= 0) {
-    return;
+    return libtextclassifier3::Status::OK;
   }
   IcingTimer timer;
 
-  TermIdHitPair::Value* array_start =
+  auto array_start_or =
       hit_buffer_.GetMutableMem<TermIdHitPair::Value>(0, header_->cur_size());
+  if (!array_start_or.ok()) {
+    // The error here means an allocation of the hit buffer array failed, but
+    // this should NEVER happen since the hit buffer size should be at least
+    // header_->cur_size() at this moment.
+    ICING_LOG(ERROR)
+        << "GetMutableMem failed in SortHitsImpl, which should never happen: "
+        << array_start_or.status().error_message();
+    return array_start_or.status();
+  }
+  TermIdHitPair::Value* array_start = array_start_or.ValueOrDie();
   TermIdHitPair::Value* sort_start = array_start + header_->searchable_end();
   std::sort(sort_start, array_start + header_->cur_size());
 
@@ -653,6 +668,7 @@
 
   // Update crc in-line.
   UpdateChecksumInternal();
+  return libtextclassifier3::Status::OK;
 }
 
 libtextclassifier3::Status LiteIndex::Optimize(
@@ -665,7 +681,7 @@
   }
   // Sort the hits so that hits with the same term id will be grouped together,
   // which helps later to determine which terms will be unused after compaction.
-  SortHitsImpl();
+  ICING_RETURN_IF_ERROR(SortHitsImpl());
   uint32_t new_size = 0;
   uint32_t curr_term_id = 0;
   uint32_t curr_tvi = 0;
@@ -711,8 +727,9 @@
     // new_size is weakly less than idx so we are okay to overwrite the entry at
     // new_size, and valp should never be nullptr since it is within the already
     // allocated region of hit_buffer_.
-    TermIdHitPair::Value* valp =
-        hit_buffer_.GetMutableMem<TermIdHitPair::Value>(new_size++, 1);
+    ICING_ASSIGN_OR_RETURN(
+        TermIdHitPair::Value * valp,
+        hit_buffer_.GetMutableMem<TermIdHitPair::Value>(new_size++, 1));
     if (valp == nullptr) {
       // This really shouldn't happen since we are only writing to the already
       // allocated region of hit_buffer_. But just in case, we log and return an
@@ -741,9 +758,11 @@
     // saves an unnecessary search through the hit buffer). This is acceptable
     // because the free space will eventually be reclaimed the next time that
     // the lite index is merged with the main index.
-    if (!lexicon_.Delete(term)) {
-      return absl_ports::InternalError(
-          "Could not delete invalid terms in lite lexicon during compaction.");
+    libtextclassifier3::Status status = lexicon_.Delete(term);
+    if (!status.ok()) {
+      return absl_ports::InternalError(absl_ports::StrCat(
+          "Could not delete invalid terms in lite lexicon during compaction: ",
+          status.error_message()));
     }
   }
   return libtextclassifier3::Status::OK;
diff --git a/icing/index/lite/lite-index.h b/icing/index/lite/lite-index.h
index 280df88..e174556 100644
--- a/icing/index/lite/lite-index.h
+++ b/icing/index/lite/lite-index.h
@@ -418,7 +418,8 @@
   }
 
   // Non-locking implementation for SortHits().
-  void SortHitsImpl() ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+  libtextclassifier3::Status SortHitsImpl()
+      ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
 
   // Calculates and adds the score for a fetched hit to total_score_out, while
   // updating last_document_id (which keeps track of the last added docId so
diff --git a/icing/index/main/doc-hit-info-iterator-term-main.h b/icing/index/main/doc-hit-info-iterator-term-main.h
index e32db2a..559412e 100644
--- a/icing/index/main/doc-hit-info-iterator-term-main.h
+++ b/icing/index/main/doc-hit-info-iterator-term-main.h
@@ -15,7 +15,7 @@
 #ifndef ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_TERM_MAIN_H_
 #define ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_TERM_MAIN_H_
 
-#include <cstdint>
+#include <array>
 #include <memory>
 #include <optional>
 #include <string>
@@ -23,6 +23,7 @@
 #include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/index/hit/doc-hit-info.h"
 #include "icing/index/hit/hit.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
@@ -33,7 +34,7 @@
 namespace icing {
 namespace lib {
 
-class DocHitInfoIteratorTermMain : public DocHitInfoLeafIterator {
+class DocHitInfoIteratorTermMain : public DocHitInfoIterator {
  public:
   struct DocHitInfoAndTermFrequencyArray {
     DocHitInfo doc_hit_info;
@@ -70,13 +71,18 @@
 
   libtextclassifier3::StatusOr<TrimmedNode> TrimRightMostNode() && override;
 
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    return {};
+  }
+
   CallStats GetCallStats() const override {
     return CallStats(
         /*num_leaf_advance_calls_lite_index_in=*/0,
         /*num_leaf_advance_calls_main_index_in=*/num_advance_calls_,
         /*num_leaf_advance_calls_integer_index_in=*/0,
         /*num_leaf_advance_calls_no_index_in=*/0,
-        /*num_blocks_inspected_in=*/num_blocks_inspected_);
+        /*num_blocks_inspected_in=*/num_blocks_inspected_,
+        /*embedding_stats_in=*/{});
   }
 
   void PopulateMatchedTermsStats(
diff --git a/icing/index/main/main-index-merger.cc b/icing/index/main/main-index-merger.cc
index 8397e2c..0e5016e 100644
--- a/icing/index/main/main-index-merger.cc
+++ b/icing/index/main/main-index-merger.cc
@@ -31,6 +31,7 @@
 #include "icing/index/term-id-codec.h"
 #include "icing/legacy/core/icing-string-util.h"
 #include "icing/util/logging.h"
+#include "icing/util/math-util.h"
 #include "icing/util/status-macros.h"
 
 namespace icing {
@@ -142,15 +143,37 @@
   TermIdHitPair prev_;
 };
 
-class HitComparator {
- public:
-  explicit HitComparator(
-      const TermIdCodec& term_id_codec,
-      const std::unordered_map<uint32_t, int>& main_tvi_to_block_index)
-      : term_id_codec_(&term_id_codec),
-        main_tvi_to_block_index_(&main_tvi_to_block_index) {}
+int GetIndexBlock(
+    uint32_t term_id,
+    const std::unordered_map<uint32_t, int>& main_tvi_to_block_index,
+    const TermIdCodec& term_id_codec) {
+  auto term_info_or = term_id_codec.DecodeTermInfo(term_id);
+  if (!term_info_or.ok()) {
+    ICING_LOG(WARNING)
+        << "Unable to decode term-info during merge. This shouldn't happen.";
+    return kInvalidBlockIndex;
+  }
+  TermIdCodec::DecodedTermInfo term_info = std::move(term_info_or).ValueOrDie();
+  auto itr = main_tvi_to_block_index.find(term_info.tvi);
+  if (itr == main_tvi_to_block_index.end()) {
+    return kInvalidBlockIndex;
+  }
+  return itr->second;
+}
 
-  bool operator()(const TermIdHitPair& lhs, const TermIdHitPair& rhs) const {
+void SortHits(std::vector<TermIdHitPair>& hits,
+              const std::unordered_map<uint32_t, int>& main_tvi_to_block_index,
+              const TermIdCodec& term_id_codec) {
+  std::vector<int> indices;
+  indices.reserve(hits.size());
+  std::vector<int> index_blocks;
+  index_blocks.reserve(hits.size());
+  for (int i = 0; i < hits.size(); ++i) {
+    indices.push_back(i);
+    index_blocks.push_back(GetIndexBlock(
+        hits[i].term_id(), main_tvi_to_block_index, term_id_codec));
+  }
+  std::sort(indices.begin(), indices.end(), [&](int i, int j) {
     // Primary sort by index block. This achieves two things:
     // 1. It reduces the number of flash writes by grouping together new hits
     // for terms whose posting lists might share the same index block.
@@ -158,35 +181,14 @@
     // will be populated first (because all newly added terms have an invalid
     // block index of 0) before any new hits are added to the postings lists
     // that they backfill from.
-    int lhs_index_block = GetIndexBlock(lhs.term_id());
-    int rhs_index_block = GetIndexBlock(rhs.term_id());
-    if (lhs_index_block == rhs_index_block) {
-      // Secondary sort by term_id and hit.
-      return lhs.value() < rhs.value();
+    if (index_blocks[i] == index_blocks[j]) {
+      return hits[i].value() < hits[j].value();
     }
-    return lhs_index_block < rhs_index_block;
-  }
+    return index_blocks[i] < index_blocks[j];
+  });
 
- private:
-  int GetIndexBlock(uint32_t term_id) const {
-    auto term_info_or = term_id_codec_->DecodeTermInfo(term_id);
-    if (!term_info_or.ok()) {
-      ICING_LOG(WARNING)
-          << "Unable to decode term-info during merge. This shouldn't happen.";
-      return kInvalidBlockIndex;
-    }
-    TermIdCodec::DecodedTermInfo term_info =
-        std::move(term_info_or).ValueOrDie();
-    auto itr = main_tvi_to_block_index_->find(term_info.tvi);
-    if (itr == main_tvi_to_block_index_->end()) {
-      return kInvalidBlockIndex;
-    }
-    return itr->second;
-  }
-
-  const TermIdCodec* term_id_codec_;
-  const std::unordered_map<uint32_t, int>* main_tvi_to_block_index_;
-};
+  math_util::ApplyPermutation(indices, hits);
+}
 
 // A helper function to dedupe hits stored in hits. Suppose that the lite index
 // contained a single document with two hits in a single prefix section: "foot"
@@ -211,8 +213,7 @@
     const std::unordered_map<uint32_t, int>& main_tvi_to_block_index) {
   // Now all terms are grouped together and all hits for a term are sorted.
   // Merge equivalent hits into one.
-  std::sort(hits->begin(), hits->end(),
-            HitComparator(term_id_codec, main_tvi_to_block_index));
+  SortHits(*hits, main_tvi_to_block_index, term_id_codec);
   size_t current_offset = 0;
   HitSelector hit_selector;
   for (const TermIdHitPair& term_id_hit_pair : *hits) {
diff --git a/icing/index/main/main-index.cc b/icing/index/main/main-index.cc
index 12e8ea9..1956be4 100644
--- a/icing/index/main/main-index.cc
+++ b/icing/index/main/main-index.cc
@@ -343,8 +343,9 @@
        other_term_itr.IsValid(); other_term_itr.Advance()) {
     // If term were inserted in the main lexicon, what new branching would it
     // create? (It always creates at most one.)
-    int prefix_len = main_lexicon_->FindNewBranchingPrefixLength(
-        other_term_itr.GetKey(), /*utf8=*/true);
+    ICING_ASSIGN_OR_RETURN(int prefix_len,
+                           main_lexicon_->FindNewBranchingPrefixLength(
+                               other_term_itr.GetKey(), /*utf8=*/true));
     if (prefix_len <= 0) {
       continue;
     }
@@ -449,8 +450,9 @@
 
     // Get prefixes where there is already a branching point in the main
     // lexicon. We skip prefixes which don't already have a branching point.
-    std::vector<int> prefix_lengths = main_lexicon_->FindBranchingPrefixLengths(
-        other_term_itr.GetKey(), /*utf8=*/true);
+    ICING_ASSIGN_OR_RETURN(std::vector<int> prefix_lengths,
+                           main_lexicon_->FindBranchingPrefixLengths(
+                               other_term_itr.GetKey(), /*utf8=*/true));
 
     int buf_start = outputs.prefix_tvis_buf.size();
     // Add prefixes.
@@ -592,7 +594,8 @@
     PostingListAccessor::FinalizeResult result =
         std::move(*hit_accum).Finalize();
     if (result.id.is_valid()) {
-      main_lexicon_->SetValueAtIndex(other_tvi_main_tvi_pair.first, &result.id);
+      ICING_RETURN_IF_ERROR(main_lexicon_->SetValueAtIndex(
+          other_tvi_main_tvi_pair.first, &result.id));
     }
   }
   flash_index_storage_->set_last_indexed_docid(last_added_document_id);
@@ -648,7 +651,7 @@
   PostingListAccessor::FinalizeResult result =
       std::move(*pl_accessor).Finalize();
   if (result.id.is_valid()) {
-    main_lexicon_->SetValueAtIndex(tvi, &result.id);
+    ICING_RETURN_IF_ERROR(main_lexicon_->SetValueAtIndex(tvi, &result.id));
   }
   return libtextclassifier3::Status::OK;
 }
@@ -803,8 +806,9 @@
   // A term without exact hits indicates that it is a purely backfill term. If
   // the term is not branching in the new trie, it means backfilling is no
   // longer necessary, so that we can skip.
-  if (new_hits.empty() ||
-      (has_no_exact_hits && !new_index->main_lexicon_->IsBranchingTerm(term))) {
+  ICING_ASSIGN_OR_RETURN(bool is_branching_term,
+                         new_index->main_lexicon_->IsBranchingTerm(term));
+  if (new_hits.empty() || (has_no_exact_hits && !is_branching_term)) {
     return largest_document_id;
   }
 
diff --git a/icing/index/numeric/doc-hit-info-iterator-numeric.h b/icing/index/numeric/doc-hit-info-iterator-numeric.h
index 7cdb230..d8f83b4 100644
--- a/icing/index/numeric/doc-hit-info-iterator-numeric.h
+++ b/icing/index/numeric/doc-hit-info-iterator-numeric.h
@@ -20,16 +20,18 @@
 #include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
 #include "icing/index/numeric/numeric-index.h"
+#include "icing/schema/section.h"
 #include "icing/util/status-macros.h"
 
 namespace icing {
 namespace lib {
 
 template <typename T>
-class DocHitInfoIteratorNumeric : public DocHitInfoLeafIterator {
+class DocHitInfoIteratorNumeric : public DocHitInfoIterator {
  public:
   explicit DocHitInfoIteratorNumeric(
       std::unique_ptr<typename NumericIndex<T>::Iterator> numeric_index_iter)
@@ -53,6 +55,10 @@
         "Cannot generate suggestion if the last term is numeric operator.");
   }
 
+  std::vector<std::unique_ptr<DocHitInfoIterator>*> GetChildren() override {
+    return {};
+  }
+
   CallStats GetCallStats() const override {
     if (numeric_index_iter_ == nullptr) {
       return CallStats();
@@ -64,7 +70,8 @@
                      numeric_index_iter_->GetNumAdvanceCalls(),
                      /*num_leaf_advance_calls_no_index_in=*/0,
                      /*num_blocks_inspected_in=*/
-                     numeric_index_iter_->GetNumBlocksInspected());
+                     numeric_index_iter_->GetNumBlocksInspected(),
+                     /*embedding_stats_in=*/{});
   }
 
   std::string ToString() const override { return "test"; }
diff --git a/icing/index/numeric/integer-index-storage.cc b/icing/index/numeric/integer-index-storage.cc
index 9566737..033c64a 100644
--- a/icing/index/numeric/integer-index-storage.cc
+++ b/icing/index/numeric/integer-index-storage.cc
@@ -46,6 +46,7 @@
 #include "icing/schema/section.h"
 #include "icing/store/document-id.h"
 #include "icing/util/crc32.h"
+#include "icing/util/logging.h"
 #include "icing/util/status-macros.h"
 
 namespace icing {
@@ -928,7 +929,13 @@
   // Validate other values of info and options.
   // Magic should be consistent with the codebase.
   if (integer_index_storage->info().magic != Info::kMagic) {
-    return absl_ports::FailedPreconditionError("Incorrect magic value");
+    ICING_LOG(ERROR) << "Invalid header magic for IntegerIndexStorage "
+                     << integer_index_storage->working_path_
+                     << ". Expected: " << Info::kMagic
+                     << ", actual: " << integer_index_storage->info().magic;
+    return absl_ports::FailedPreconditionError(
+        absl_ports::StrCat("Invalid header magic for IntegerIndexStorage: ",
+                           integer_index_storage->working_path_));
   }
 
   return integer_index_storage;
diff --git a/icing/index/numeric/integer-index-storage_test.cc b/icing/index/numeric/integer-index-storage_test.cc
index 62c8e87..65643da 100644
--- a/icing/index/numeric/integer-index-storage_test.cc
+++ b/icing/index/numeric/integer-index-storage_test.cc
@@ -309,15 +309,17 @@
 
   // Check info section
   Info info;
-  ASSERT_TRUE(filesystem_.PRead(metadata_sfd.get(), &info, sizeof(Info),
-                                IntegerIndexStorage::kInfoMetadataFileOffset));
+  ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), &info, sizeof(Info),
+                                IntegerIndexStorage::kInfoMetadataFileOffset),
+              Eq(sizeof(Info)));
   EXPECT_THAT(info.magic, Eq(Info::kMagic));
   EXPECT_THAT(info.num_data, Eq(0));
 
   // Check crcs section
   Crcs crcs;
-  ASSERT_TRUE(filesystem_.PRead(metadata_sfd.get(), &crcs, sizeof(Crcs),
-                                IntegerIndexStorage::kCrcsMetadataFileOffset));
+  ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), &crcs, sizeof(Crcs),
+                                IntegerIndexStorage::kCrcsMetadataFileOffset),
+              Eq(sizeof(Crcs)));
   // # of elements in sorted_buckets should be 1, so it should have non-zero
   // all storages crc value.
   EXPECT_THAT(crcs.component_crcs.storages_crc, Ne(0));
@@ -518,8 +520,9 @@
   ASSERT_TRUE(metadata_sfd.is_valid());
 
   Crcs crcs;
-  ASSERT_TRUE(filesystem_.PRead(metadata_sfd.get(), &crcs, sizeof(Crcs),
-                                IntegerIndexStorage::kCrcsMetadataFileOffset));
+  ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), &crcs, sizeof(Crcs),
+                                IntegerIndexStorage::kCrcsMetadataFileOffset),
+              Eq(sizeof(Crcs)));
 
   // Manually corrupt all_crc
   crcs.all_crc += kCorruptedValueOffset;
@@ -567,8 +570,9 @@
   ASSERT_TRUE(metadata_sfd.is_valid());
 
   Info info;
-  ASSERT_TRUE(filesystem_.PRead(metadata_sfd.get(), &info, sizeof(Info),
-                                IntegerIndexStorage::kInfoMetadataFileOffset));
+  ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), &info, sizeof(Info),
+                                IntegerIndexStorage::kInfoMetadataFileOffset),
+              Eq(sizeof(Info)));
 
   // Modify info, but don't update the checksum. This would be similar to
   // corruption of info.
@@ -1513,13 +1517,14 @@
   while (iter1->Advance().ok()) {
     // Advance all hits.
   }
-  EXPECT_THAT(
-      iter1->GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/0,
-          /*num_leaf_advance_calls_main_index=*/0,
-          /*num_leaf_advance_calls_integer_index=*/5,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/2));
+  EXPECT_THAT(iter1->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/0,
+                  /*num_leaf_advance_calls_main_index=*/0,
+                  /*num_leaf_advance_calls_integer_index=*/5,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/2,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // GetIterator for range [-1000, -100] and Advance all. Since we only have to
   // read bucket (-1000,-100), there will be 3 advance calls and 1 block
@@ -1530,13 +1535,14 @@
   while (iter2->Advance().ok()) {
     // Advance all hits.
   }
-  EXPECT_THAT(
-      iter2->GetCallStats(),
-      EqualsDocHitInfoIteratorCallStats(
-          /*num_leaf_advance_calls_lite_index=*/0,
-          /*num_leaf_advance_calls_main_index=*/0,
-          /*num_leaf_advance_calls_integer_index=*/3,
-          /*num_leaf_advance_calls_no_index=*/0, /*num_blocks_inspected=*/1));
+  EXPECT_THAT(iter2->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/0,
+                  /*num_leaf_advance_calls_main_index=*/0,
+                  /*num_leaf_advance_calls_integer_index=*/3,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/1,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 }
 
 TEST_P(IntegerIndexStorageTest, IteratorCallStatsSingleBucketChainedBlocks) {
@@ -1570,13 +1576,14 @@
   while (iter1->Advance().ok()) {
     // Advance all hits.
   }
-  EXPECT_THAT(iter1->GetCallStats(),
-              EqualsDocHitInfoIteratorCallStats(
-                  /*num_leaf_advance_calls_lite_index=*/0,
-                  /*num_leaf_advance_calls_main_index=*/0,
-                  /*num_leaf_advance_calls_integer_index=*/num_keys_to_add,
-                  /*num_leaf_advance_calls_no_index=*/0,
-                  expected_num_blocks_inspected));
+  EXPECT_THAT(
+      iter1->GetCallStats(),
+      EqualsDocHitInfoIteratorCallStats(
+          /*num_leaf_advance_calls_lite_index=*/0,
+          /*num_leaf_advance_calls_main_index=*/0,
+          /*num_leaf_advance_calls_integer_index=*/num_keys_to_add,
+          /*num_leaf_advance_calls_no_index=*/0, expected_num_blocks_inspected,
+          DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // GetIterator for range [1, 1] and Advance all. Although there is only 1
   // relevant data, we still have to inspect the entire bucket and its posting
@@ -1587,13 +1594,14 @@
   while (iter2->Advance().ok()) {
     // Advance all hits.
   }
-  EXPECT_THAT(iter2->GetCallStats(),
-              EqualsDocHitInfoIteratorCallStats(
-                  /*num_leaf_advance_calls_lite_index=*/0,
-                  /*num_leaf_advance_calls_main_index=*/0,
-                  /*num_leaf_advance_calls_integer_index=*/num_keys_to_add,
-                  /*num_leaf_advance_calls_no_index=*/0,
-                  expected_num_blocks_inspected));
+  EXPECT_THAT(
+      iter2->GetCallStats(),
+      EqualsDocHitInfoIteratorCallStats(
+          /*num_leaf_advance_calls_lite_index=*/0,
+          /*num_leaf_advance_calls_main_index=*/0,
+          /*num_leaf_advance_calls_integer_index=*/num_keys_to_add,
+          /*num_leaf_advance_calls_no_index=*/0, expected_num_blocks_inspected,
+          DocHitInfoIterator::CallStats::EmbeddingStats()));
 }
 
 TEST_P(IntegerIndexStorageTest, SplitBuckets) {
diff --git a/icing/index/numeric/integer-index.cc b/icing/index/numeric/integer-index.cc
index 62fb215..36148d4 100644
--- a/icing/index/numeric/integer-index.cc
+++ b/icing/index/numeric/integer-index.cc
@@ -35,6 +35,7 @@
 #include "icing/index/numeric/posting-list-integer-index-serializer.h"
 #include "icing/store/document-id.h"
 #include "icing/util/crc32.h"
+#include "icing/util/logging.h"
 #include "icing/util/status-macros.h"
 
 namespace icing {
@@ -494,7 +495,13 @@
 
   // Validate magic.
   if (integer_index->info().magic != Info::kMagic) {
-    return absl_ports::FailedPreconditionError("Incorrect magic value");
+    ICING_LOG(ERROR) << "Invalid header magic for IntegerIndex "
+                     << integer_index->working_path_
+                     << ". Expected: " << Info::kMagic
+                     << ", actual: " << integer_index->info().magic;
+    return absl_ports::FailedPreconditionError(
+        absl_ports::StrCat("Invalid header magic for IntegerIndex: ",
+                           integer_index->working_path_));
   }
 
   // If num_data_threshold_for_bucket_split mismatches, then return error to let
diff --git a/icing/index/numeric/integer-index_test.cc b/icing/index/numeric/integer-index_test.cc
index 619abc4..ef48314 100644
--- a/icing/index/numeric/integer-index_test.cc
+++ b/icing/index/numeric/integer-index_test.cc
@@ -20,15 +20,20 @@
 #include <string>
 #include <string_view>
 #include <type_traits>
+#include <unordered_set>
+#include <utility>
 #include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "icing/absl_ports/canonical_errors.h"
+#include "icing/absl_ports/str_cat.h"
 #include "icing/document-builder.h"
 #include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
+#include "icing/file/persistent-storage.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
@@ -36,6 +41,7 @@
 #include "icing/index/numeric/integer-index-storage.h"
 #include "icing/index/numeric/numeric-index.h"
 #include "icing/index/numeric/posting-list-integer-index-serializer.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/schema-builder.h"
@@ -46,6 +52,10 @@
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/clock.h"
+#include "icing/util/crc32.h"
+#include "icing/util/document-util.h"
+#include "icing/util/status-macros.h"
 
 namespace icing {
 namespace lib {
@@ -92,14 +102,18 @@
         filesystem_.CreateDirectoryRecursively(document_store_dir.c_str()));
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult doc_store_create_result,
-        DocumentStore::Create(&filesystem_, document_store_dir, &clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, document_store_dir, &clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     doc_store_ = std::move(doc_store_create_result.document_store);
   }
 
@@ -167,14 +181,18 @@
 
     ICING_ASSIGN_OR_RETURN(
         DocumentStore::CreateResult doc_store_create_result,
-        DocumentStore::Create(&filesystem_, document_store_dir, &clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, document_store_dir, &clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     doc_store_ = std::move(doc_store_create_result.document_store);
     return std::move(doc_store_optimize_result.document_id_old_to_new);
   }
@@ -413,49 +431,51 @@
   // Put 11 docs of "TypeA" into the document store.
   DocumentProto doc =
       DocumentBuilder().SetKey("ns1", "uri0").SetSchema("TypeA").Build();
-  ICING_ASSERT_OK(this->doc_store_->Put(doc));
   ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri1").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri2").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri3").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri4").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri5").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri6").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri7").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri8").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri9").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri10").Build()));
+      this->doc_store_->Put(document_util::CreateDocumentWrapper(doc)));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri1").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri2").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri3").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri4").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri5").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri6").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri7").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri8").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri9").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri10").Build())));
 
   // Put 5 docs of "TypeB" into the document store.
   doc = DocumentBuilder(doc).SetUri("uri11").SetSchema("TypeB").Build();
-  ICING_ASSERT_OK(this->doc_store_->Put(doc));
   ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri12").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri13").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri14").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri15").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri16").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri17").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri18").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri19").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri20").Build()));
+      this->doc_store_->Put(document_util::CreateDocumentWrapper(doc)));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri12").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri13").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri14").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri15").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri16").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri17").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri18").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri19").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri20").Build())));
 
   // Ids are assigned alphabetically, so the property ids are:
   // TypeA.desiredProperty = 0
@@ -1180,8 +1200,9 @@
 
   // Check info section
   Info info;
-  ASSERT_TRUE(filesystem_.PRead(metadata_sfd.get(), &info, sizeof(Info),
-                                IntegerIndex::kInfoMetadataFileOffset));
+  ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), &info, sizeof(Info),
+                                IntegerIndex::kInfoMetadataFileOffset),
+              Eq(sizeof(Info)));
   EXPECT_THAT(info.magic, Eq(Info::kMagic));
   EXPECT_THAT(info.last_added_document_id, Eq(kInvalidDocumentId));
   EXPECT_THAT(info.num_data_threshold_for_bucket_split,
@@ -1189,8 +1210,9 @@
 
   // Check crcs section
   Crcs crcs;
-  ASSERT_TRUE(filesystem_.PRead(metadata_sfd.get(), &crcs, sizeof(Crcs),
-                                IntegerIndex::kCrcsMetadataFileOffset));
+  ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), &crcs, sizeof(Crcs),
+                                IntegerIndex::kCrcsMetadataFileOffset),
+              Eq(sizeof(Crcs)));
   // There are no storages initially, so storages_crc should be 0.
   EXPECT_THAT(crcs.component_crcs.storages_crc, Eq(0));
   EXPECT_THAT(crcs.component_crcs.info_crc,
@@ -1376,8 +1398,9 @@
   ASSERT_TRUE(metadata_sfd.is_valid());
 
   Crcs crcs;
-  ASSERT_TRUE(filesystem_.PRead(metadata_sfd.get(), &crcs, sizeof(Crcs),
-                                IntegerIndex::kCrcsMetadataFileOffset));
+  ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), &crcs, sizeof(Crcs),
+                                IntegerIndex::kCrcsMetadataFileOffset),
+              Eq(sizeof(Crcs)));
 
   // Manually corrupt all_crc
   crcs.all_crc += kCorruptedValueOffset;
@@ -1425,8 +1448,9 @@
   ASSERT_TRUE(metadata_sfd.is_valid());
 
   Info info;
-  ASSERT_TRUE(filesystem_.PRead(metadata_sfd.get(), &info, sizeof(Info),
-                                IntegerIndex::kInfoMetadataFileOffset));
+  ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), &info, sizeof(Info),
+                                IntegerIndex::kInfoMetadataFileOffset),
+              Eq(sizeof(Info)));
 
   // Modify info, but don't update the checksum. This would be similar to
   // corruption of info.
@@ -1656,49 +1680,51 @@
   // Put 11 docs of "TypeA" into the document store.
   DocumentProto doc =
       DocumentBuilder().SetKey("ns1", "uri0").SetSchema("TypeA").Build();
-  ICING_ASSERT_OK(this->doc_store_->Put(doc));
   ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri1").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri2").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri3").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri4").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri5").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri6").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri7").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri8").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri9").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri10").Build()));
+      this->doc_store_->Put(document_util::CreateDocumentWrapper(doc)));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri1").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri2").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri3").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri4").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri5").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri6").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri7").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri8").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri9").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri10").Build())));
 
   // Put 10 docs of "TypeB" into the document store.
   doc = DocumentBuilder(doc).SetUri("uri11").SetSchema("TypeB").Build();
-  ICING_ASSERT_OK(this->doc_store_->Put(doc));
   ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri12").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri13").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri14").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri15").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri16").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri17").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri18").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri19").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri20").Build()));
+      this->doc_store_->Put(document_util::CreateDocumentWrapper(doc)));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri12").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri13").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri14").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri15").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri16").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri17").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri18").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri19").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri20").Build())));
 
   {
     ICING_ASSERT_OK_AND_ASSIGN(
@@ -2040,49 +2066,51 @@
   // Put 11 docs of "TypeA" into the document store.
   DocumentProto doc =
       DocumentBuilder().SetKey("ns1", "uri0").SetSchema("TypeA").Build();
-  ICING_ASSERT_OK(this->doc_store_->Put(doc));
   ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri1").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri2").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri3").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri4").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri5").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri6").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri7").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri8").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri9").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri10").Build()));
+      this->doc_store_->Put(document_util::CreateDocumentWrapper(doc)));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri1").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri2").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri3").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri4").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri5").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri6").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri7").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri8").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri9").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri10").Build())));
 
   // Put 10 docs of "TypeB" into the document store.
   doc = DocumentBuilder(doc).SetUri("uri11").SetSchema("TypeB").Build();
-  ICING_ASSERT_OK(this->doc_store_->Put(doc));
   ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri12").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri13").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri14").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri15").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri16").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri17").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri18").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri19").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri20").Build()));
+      this->doc_store_->Put(document_util::CreateDocumentWrapper(doc)));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri12").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri13").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri14").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri15").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri16").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri17").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri18").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri19").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri20").Build())));
 
   {
     ICING_ASSERT_OK_AND_ASSIGN(
@@ -2330,27 +2358,28 @@
   // Put 11 docs of "TypeA" into the document store.
   DocumentProto doc =
       DocumentBuilder().SetKey("ns1", "uri0").SetSchema("TypeA").Build();
-  ICING_ASSERT_OK(this->doc_store_->Put(doc));
   ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri1").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri2").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri3").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri4").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri5").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri6").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri7").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri8").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri9").Build()));
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri10").Build()));
+      this->doc_store_->Put(document_util::CreateDocumentWrapper(doc)));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri1").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri2").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri3").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri4").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri5").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri6").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri7").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri8").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri9").Build())));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri10").Build())));
 
   {
     ICING_ASSERT_OK_AND_ASSIGN(
@@ -2446,7 +2475,8 @@
   // Add a new doc (docid==5) and a hit for desiredProperty. This should still
   // be placed into the wildcard integer storage.
   doc = DocumentBuilder().SetKey("ns1", "uri11").SetSchema("TypeA").Build();
-  ICING_ASSERT_OK(this->doc_store_->Put(doc));
+  ICING_ASSERT_OK(
+      this->doc_store_->Put(document_util::CreateDocumentWrapper(doc)));
   Index(integer_index.get(), desired_property, /*document_id=*/5,
         typea_desired_prop_id, /*keys=*/{12});
   EXPECT_THAT(integer_index->num_property_indices(), Eq(1));
@@ -2458,8 +2488,8 @@
 
   // Add a new doc (docid==6) and a hit for undesiredProperty. This should still
   // be placed into the wildcard integer storage.
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri12").Build()));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri12").Build())));
   Index(integer_index.get(), undesired_property, /*document_id=*/6,
         typea_undesired_prop_id, /*keys=*/{3});
   EXPECT_THAT(integer_index->num_property_indices(), Eq(1));
@@ -2472,8 +2502,8 @@
 
   // Add a new doc (docid==7) and a hit for otherProperty1. This should be given
   // its own individual storage.
-  ICING_ASSERT_OK(
-      this->doc_store_->Put(DocumentBuilder(doc).SetUri("uri13").Build()));
+  ICING_ASSERT_OK(this->doc_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder(doc).SetUri("uri13").Build())));
   Index(integer_index.get(), other_property_1, /*document_id=*/7,
         typea_other1_prop_id, /*keys=*/{3});
   EXPECT_THAT(integer_index->num_property_indices(), Eq(2));
@@ -2519,7 +2549,8 @@
                   /*num_leaf_advance_calls_main_index=*/0,
                   /*num_leaf_advance_calls_integer_index=*/1,
                   /*num_leaf_advance_calls_no_index=*/0,
-                  /*num_blocks_inspected=*/1));
+                  /*num_blocks_inspected=*/1,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 1st Advance().
   ICING_ASSERT_OK(iter->Advance());
@@ -2529,7 +2560,8 @@
                   /*num_leaf_advance_calls_main_index=*/0,
                   /*num_leaf_advance_calls_integer_index=*/2,
                   /*num_leaf_advance_calls_no_index=*/0,
-                  /*num_blocks_inspected=*/1));
+                  /*num_blocks_inspected=*/1,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 2nd Advance().
   ICING_ASSERT_OK(iter->Advance());
@@ -2539,7 +2571,8 @@
                   /*num_leaf_advance_calls_main_index=*/0,
                   /*num_leaf_advance_calls_integer_index=*/3,
                   /*num_leaf_advance_calls_no_index=*/0,
-                  /*num_blocks_inspected=*/1));
+                  /*num_blocks_inspected=*/1,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 3rd Advance().
   ICING_ASSERT_OK(iter->Advance());
@@ -2549,7 +2582,8 @@
                   /*num_leaf_advance_calls_main_index=*/0,
                   /*num_leaf_advance_calls_integer_index=*/4,
                   /*num_leaf_advance_calls_no_index=*/0,
-                  /*num_blocks_inspected=*/1));
+                  /*num_blocks_inspected=*/1,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 4th Advance().
   ICING_ASSERT_OK(iter->Advance());
@@ -2559,7 +2593,8 @@
                   /*num_leaf_advance_calls_main_index=*/0,
                   /*num_leaf_advance_calls_integer_index=*/4,
                   /*num_leaf_advance_calls_no_index=*/0,
-                  /*num_blocks_inspected=*/1));
+                  /*num_blocks_inspected=*/1,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 5th Advance().
   ASSERT_THAT(iter->Advance(),
@@ -2570,7 +2605,8 @@
                   /*num_leaf_advance_calls_main_index=*/0,
                   /*num_leaf_advance_calls_integer_index=*/4,
                   /*num_leaf_advance_calls_no_index=*/0,
-                  /*num_blocks_inspected=*/1));
+                  /*num_blocks_inspected=*/1,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 }
 
 TEST_P(IntegerIndexTest, IteratorCallStatsNonExistingProperty) {
@@ -2603,7 +2639,8 @@
                   /*num_leaf_advance_calls_main_index=*/0,
                   /*num_leaf_advance_calls_integer_index=*/0,
                   /*num_leaf_advance_calls_no_index=*/0,
-                  /*num_blocks_inspected=*/0));
+                  /*num_blocks_inspected=*/0,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 
   // 1st Advance().
   ASSERT_THAT(iter->Advance(),
@@ -2614,7 +2651,8 @@
                   /*num_leaf_advance_calls_main_index=*/0,
                   /*num_leaf_advance_calls_integer_index=*/0,
                   /*num_leaf_advance_calls_no_index=*/0,
-                  /*num_blocks_inspected=*/0));
+                  /*num_blocks_inspected=*/0,
+                  DocHitInfoIterator::CallStats::EmbeddingStats()));
 }
 
 INSTANTIATE_TEST_SUITE_P(
diff --git a/icing/index/property-existence-indexing-handler.cc b/icing/index/property-existence-indexing-handler.cc
index 0b4d87f..408897a 100644
--- a/icing/index/property-existence-indexing-handler.cc
+++ b/icing/index/property-existence-indexing-handler.cc
@@ -92,7 +92,8 @@
       index_.Edit(document_id, /*section_id=*/0, /*namespace_id=*/0);
   std::unordered_set<std::string> meta_tokens;
   ConstructPropertyExistenceMetaToken(
-      /*current_path=*/"", tokenized_document.document(), meta_tokens);
+      /*current_path=*/"", tokenized_document.document_wrapper().document(),
+      meta_tokens);
   for (const std::string& meta_token : meta_tokens) {
     status = editor.BufferTerm(meta_token, TermMatchType::EXACT_ONLY);
     if (!status.ok()) {
diff --git a/icing/index/property-existence-indexing-handler_test.cc b/icing/index/property-existence-indexing-handler_test.cc
index 8905bc5..208aebf 100644
--- a/icing/index/property-existence-indexing-handler_test.cc
+++ b/icing/index/property-existence-indexing-handler_test.cc
@@ -35,6 +35,7 @@
 #include "icing/index/index.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
 #include "icing/legacy/index/icing-filesystem.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/document_wrapper.pb.h"
@@ -160,14 +161,18 @@
         filesystem_.CreateDirectoryRecursively(document_store_dir_.c_str()));
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult doc_store_create_result,
-        DocumentStore::Create(&filesystem_, document_store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, document_store_dir_, &fake_clock_,
+            schema_store_.get(), feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(doc_store_create_result.document_store);
   }
 
@@ -219,7 +224,8 @@
                          /*lite_index_sort_size=*/1024 * 8);
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Index> index,
-      Index::Create(options, &filesystem_, &icing_filesystem_));
+      Index::Create(options, &filesystem_, &icing_filesystem_,
+                    feature_flags_.get()));
 
   // Create a document with every property.
   DocumentProto document0 =
@@ -249,27 +255,33 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document0,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document0)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document0)));
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document1,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document1)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document1)));
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document2,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document2)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document2)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result0,
-      document_store_->Put(tokenized_document0.document()));
+      document_store_->Put(tokenized_document0.document_wrapper()));
   DocumentId document_id0 = put_result0.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(tokenized_document1.document()));
+      document_store_->Put(tokenized_document1.document_wrapper()));
   DocumentId document_id1 = put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(tokenized_document2.document()));
+      document_store_->Put(tokenized_document2.document_wrapper()));
   DocumentId document_id2 = put_result2.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -321,7 +333,8 @@
                          /*lite_index_sort_size=*/1024 * 8);
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Index> index,
-      Index::Create(options, &filesystem_, &icing_filesystem_));
+      Index::Create(options, &filesystem_, &icing_filesystem_,
+                    feature_flags_.get()));
 
   // Create a complex nested root_document with the following property paths.
   // - name
@@ -380,11 +393,13 @@
   // Handle root_document
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_root_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(root_document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(root_document)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(tokenized_root_document.document()));
+      document_store_->Put(tokenized_root_document.document_wrapper()));
   DocumentId document_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<PropertyExistenceIndexingHandler> handler,
@@ -460,7 +475,8 @@
                          /*lite_index_sort_size=*/1024 * 8);
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Index> index,
-      Index::Create(options, &filesystem_, &icing_filesystem_));
+      Index::Create(options, &filesystem_, &icing_filesystem_,
+                    feature_flags_.get()));
 
   // Create a document with one empty body.
   DocumentProto document0 =
@@ -486,27 +502,33 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document0,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document0)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document0)));
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document1,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document1)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document1)));
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document2,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document2)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document2)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result0,
-      document_store_->Put(tokenized_document0.document()));
+      document_store_->Put(tokenized_document0.document_wrapper()));
   DocumentId document_id0 = put_result0.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(tokenized_document1.document()));
+      document_store_->Put(tokenized_document1.document_wrapper()));
   DocumentId document_id1 = put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(tokenized_document2.document()));
+      document_store_->Put(tokenized_document2.document_wrapper()));
   DocumentId document_id2 = put_result2.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
diff --git a/icing/index/term-indexing-handler_test.cc b/icing/index/term-indexing-handler_test.cc
index 577e80b..02f8164 100644
--- a/icing/index/term-indexing-handler_test.cc
+++ b/icing/index/term-indexing-handler_test.cc
@@ -39,6 +39,7 @@
 #include "icing/index/iterator/doc-hit-info-iterator.h"
 #include "icing/index/property-existence-indexing-handler.h"
 #include "icing/legacy/index/icing-filesystem.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/document_wrapper.pb.h"
@@ -176,14 +177,18 @@
         filesystem_.CreateDirectoryRecursively(document_store_dir_.c_str()));
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult doc_store_create_result,
-        DocumentStore::Create(&filesystem_, document_store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, document_store_dir_, &fake_clock_,
+            schema_store_.get(), feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(doc_store_create_result.document_store);
   }
 
@@ -249,7 +254,8 @@
                          /*lite_index_sort_size=*/1024 * 8);
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Index> index,
-      Index::Create(options, &filesystem_, &icing_filesystem_));
+      Index::Create(options, &filesystem_, &icing_filesystem_,
+                    feature_flags_.get()));
 
   DocumentProto document =
       DocumentBuilder()
@@ -261,12 +267,14 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(tokenized_document.document()));
+      document_store_->Put(tokenized_document.document_wrapper()));
   DocumentId document_id = put_result.new_document_id;
 
   EXPECT_THAT(index->last_added_document_id(), Eq(kInvalidDocumentId));
@@ -314,7 +322,8 @@
                          /*lite_index_sort_size=*/1024 * 8);
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Index> index,
-      Index::Create(options, &filesystem_, &icing_filesystem_));
+      Index::Create(options, &filesystem_, &icing_filesystem_,
+                    feature_flags_.get()));
 
   DocumentProto document =
       DocumentBuilder()
@@ -326,12 +335,14 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(tokenized_document.document()));
+      document_store_->Put(tokenized_document.document_wrapper()));
   DocumentId document_id = put_result.new_document_id;
 
   EXPECT_THAT(index->last_added_document_id(), Eq(kInvalidDocumentId));
@@ -378,7 +389,8 @@
                          /*lite_index_sort_size=*/64);
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Index> index,
-      Index::Create(options, &filesystem_, &icing_filesystem_));
+      Index::Create(options, &filesystem_, &icing_filesystem_,
+                    feature_flags_.get()));
 
   DocumentProto document0 =
       DocumentBuilder()
@@ -405,29 +417,35 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document0,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document0)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document0)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result0,
-      document_store_->Put(tokenized_document0.document()));
+      document_store_->Put(tokenized_document0.document_wrapper()));
   DocumentId document_id0 = put_result0.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document1,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document1)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document1)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(tokenized_document1.document()));
+      document_store_->Put(tokenized_document1.document_wrapper()));
   DocumentId document_id1 = put_result1.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document2,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document2)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document2)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(tokenized_document2.document()));
+      document_store_->Put(tokenized_document2.document_wrapper()));
   DocumentId document_id2 = put_result2.new_document_id;
   EXPECT_THAT(index->last_added_document_id(), Eq(kInvalidDocumentId));
 
@@ -526,7 +544,8 @@
                          /*lite_index_sort_size=*/64);
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Index> index,
-      Index::Create(options, &filesystem_, &icing_filesystem_));
+      Index::Create(options, &filesystem_, &icing_filesystem_,
+                    feature_flags_.get()));
 
   DocumentProto document0 =
       DocumentBuilder()
@@ -553,29 +572,35 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document0,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document0)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document0)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result0,
-      document_store_->Put(tokenized_document0.document()));
+      document_store_->Put(tokenized_document0.document_wrapper()));
   DocumentId document_id0 = put_result0.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document1,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document1)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document1)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(tokenized_document1.document()));
+      document_store_->Put(tokenized_document1.document_wrapper()));
   DocumentId document_id1 = put_result1.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document2,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document2)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document2)));
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(tokenized_document2.document()));
+      document_store_->Put(tokenized_document2.document_wrapper()));
   DocumentId document_id2 = put_result2.new_document_id;
   EXPECT_THAT(index->last_added_document_id(), Eq(kInvalidDocumentId));
 
@@ -612,7 +637,8 @@
                            /*lite_index_sort_at_indexing=*/true,
                            /*lite_index_sort_size=*/64);
   ICING_ASSERT_OK_AND_ASSIGN(
-      index, Index::Create(options, &filesystem_, &icing_filesystem_));
+      index, Index::Create(options, &filesystem_, &icing_filesystem_,
+                           feature_flags_.get()));
 
   // Verify that the HitBuffer has been sorted after initializing with
   // sort_at_indexing enabled.
diff --git a/icing/jni/icing-search-engine-jni.cc b/icing/jni/icing-search-engine-jni.cc
index c17ea0b..dfa28b6 100644
--- a/icing/jni/icing-search-engine-jni.cc
+++ b/icing/jni/icing-search-engine-jni.cc
@@ -14,6 +14,8 @@
 
 #include <jni.h>
 
+#include <cstdint>
+#include <memory>
 #include <string_view>
 #include <utility>
 
@@ -29,8 +31,10 @@
 #include "icing/proto/schema.pb.h"
 #include "icing/proto/scoring.pb.h"
 #include "icing/proto/search.pb.h"
+#include "icing/proto/status.pb.h"
 #include "icing/proto/storage.pb.h"
 #include "icing/proto/usage.pb.h"
+#include "icing/util/clock.h"
 #include "icing/util/logging.h"
 #include <google/protobuf/message_lite.h>
 
@@ -126,10 +130,8 @@
 
   return SerializeProtoToJniByteArray(env, set_schema_result_proto);
 }
-// TODO : b/337913932 - pre-register this API once Jetpack build is dropped back
-// into g3
-JNIEXPORT jbyteArray JNICALL
-Java_com_google_android_icing_IcingSearchEngineImpl_nativeSetSchemaWithRequestProto(
+
+jbyteArray nativeSetSchemaWithRequestProto(
     JNIEnv* env, jclass clazz, jobject object,
     jbyteArray set_schema_request_bytes) {
   icing::lib::IcingSearchEngine* icing =
@@ -202,10 +204,8 @@
   return SerializeProtoToJniByteArray(env, put_result_proto);
 }
 
-JNIEXPORT jbyteArray JNICALL
-Java_com_google_android_icing_IcingSearchEngineImpl_nativeBatchPut(
-    JNIEnv* env, jclass clazz, jobject object,
-    jbyteArray put_document_request_bytes) {
+jbyteArray nativeBatchPut(JNIEnv* env, jclass clazz, jobject object,
+                          jbyteArray put_document_request_bytes) {
   icing::lib::IcingSearchEngine* icing =
       GetIcingSearchEnginePointer(env, object);
 
@@ -219,6 +219,8 @@
 
   icing::lib::BatchPutResultProto batch_put_result_proto =
       icing->BatchPut(std::move(put_document_request));
+  batch_put_result_proto.mutable_status()->set_code(
+      icing::lib::StatusProto::OK);
 
   return SerializeProtoToJniByteArray(env, batch_put_result_proto);
 }
@@ -244,6 +246,24 @@
   return SerializeProtoToJniByteArray(env, get_result_proto);
 }
 
+jbyteArray nativeBatchGet(JNIEnv* env, jclass clazz, jobject object,
+                          jbyteArray result_spec_bytes) {
+  icing::lib::IcingSearchEngine* icing =
+      GetIcingSearchEnginePointer(env, object);
+
+  icing::lib::GetResultSpecProto get_result_spec;
+  if (!ParseProtoFromJniByteArray(env, result_spec_bytes, &get_result_spec)) {
+    ICING_LOG(icing::lib::ERROR)
+        << "Failed to parse GetResultSpecProto in nativeGet";
+    return nullptr;
+  }
+
+  icing::lib::BatchGetResultProto batch_get_result_proto =
+      icing->BatchGet(std::move(get_result_spec));
+
+  return SerializeProtoToJniByteArray(env, batch_get_result_proto);
+}
+
 jbyteArray nativeReportUsage(JNIEnv* env, jclass clazz, jobject object,
                              jbyteArray usage_report_bytes) {
   icing::lib::IcingSearchEngine* icing =
@@ -295,14 +315,57 @@
   return SerializeProtoToJniByteArray(env, next_page_result_proto);
 }
 
+// TODO: b/417644758 - pre-register this method.
+JNIEXPORT jbyteArray JNICALL
+Java_com_google_android_icing_IcingSearchEngineImpl_nativeGetNextPageWithRequestProto(
+    JNIEnv* env, jclass clazz, jobject object,
+    jbyteArray get_next_page_request_bytes,
+    jlong java_to_native_start_timestamp_ms) {
+  icing::lib::IcingSearchEngine* icing =
+      GetIcingSearchEnginePointer(env, object);
+
+  const std::unique_ptr<const icing::lib::Clock> clock =
+      std::make_unique<icing::lib::Clock>();
+  int32_t java_to_native_jni_latency_ms =
+      clock->GetSystemTimeMilliseconds() - java_to_native_start_timestamp_ms;
+
+  icing::lib::GetNextPageRequestProto get_next_page_request_proto;
+  if (!ParseProtoFromJniByteArray(env, get_next_page_request_bytes,
+                                  &get_next_page_request_proto)) {
+    ICING_LOG(icing::lib::ERROR) << "Failed to parse GetNextPageRequestProto "
+                                    "in nativeGetNextPageWithRequestProto";
+    return nullptr;
+  }
+  icing::lib::SearchResultProto next_page_result_proto =
+      icing->GetNextPage(std::move(get_next_page_request_proto));
+
+  icing::lib::QueryStatsProto* query_stats =
+      next_page_result_proto.mutable_query_stats();
+  query_stats->set_java_to_native_jni_latency_ms(java_to_native_jni_latency_ms);
+  query_stats->set_native_to_java_start_timestamp_ms(
+      clock->GetSystemTimeMilliseconds());
+
+  return SerializeProtoToJniByteArray(env, next_page_result_proto);
+}
+
 void nativeInvalidateNextPageToken(JNIEnv* env, jclass clazz, jobject object,
                                    jlong next_page_token) {
   icing::lib::IcingSearchEngine* icing =
       GetIcingSearchEnginePointer(env, object);
 
   icing->InvalidateNextPageToken(next_page_token);
+}
 
-  return;
+// TODO(b/384947619) - pre-register this method.
+JNIEXPORT jbyteArray JNICALL
+Java_com_google_android_icing_IcingSearchEngineImpl_nativeHandleExpiredDocuments(
+    JNIEnv* env, jclass clazz, jobject object) {
+  icing::lib::IcingSearchEngine* icing =
+      GetIcingSearchEnginePointer(env, object);
+
+  icing::lib::HandleExpiredDocumentsResultProto result_proto =
+      icing->HandleExpiredDocuments();
+  return SerializeProtoToJniByteArray(env, result_proto);
 }
 
 // TODO(b/273591938): Change this API back to the pre-registered API.
@@ -374,6 +437,41 @@
   return SerializeProtoToJniByteArray(env, blob_result_proto);
 }
 
+// TODO : b/434206770 - pre-register this API once Jetpack build is dropped back
+// into g3
+JNIEXPORT jbyteArray JNICALL
+Java_com_google_android_icing_IcingSearchEngineImpl_nativeGetAllBlobInfos(JNIEnv* env, jclass clazz,
+                                                  jobject object) {
+  icing::lib::IcingSearchEngine* icing =
+      GetIcingSearchEnginePointer(env, object);
+
+  icing::lib::BlobProto blob_result_proto = icing->GetAllBlobInfos();
+
+  return SerializeProtoToJniByteArray(env, blob_result_proto);
+}
+
+// TODO : b/434206770 - pre-register this API once Jetpack build is dropped back
+// into g3
+JNIEXPORT jbyteArray JNICALL
+Java_com_google_android_icing_IcingSearchEngineImpl_nativePutBlobInfos(JNIEnv* env, jclass clazz,
+                                                jobject object,
+                                                jbyteArray blob_bytes) {
+  icing::lib::IcingSearchEngine* icing =
+      GetIcingSearchEnginePointer(env, object);
+
+  icing::lib::BlobProto blob_proto;
+  if (!ParseProtoFromJniByteArray(env, blob_bytes, &blob_proto)) {
+    ICING_LOG(icing::lib::ERROR)
+        << "Failed to parse BlobInfo in nativePutBlobInfos";
+    return nullptr;
+  }
+
+  icing::lib::BlobProto blob_result_proto =
+      icing->PutBlobInfos(std::move(blob_proto));
+
+  return SerializeProtoToJniByteArray(env, blob_result_proto);
+}
+
 jbyteArray nativeSearch(JNIEnv* env, jclass clazz, jobject object,
                         jbyteArray search_spec_bytes,
                         jbyteArray scoring_spec_bytes,
@@ -532,6 +630,18 @@
   return SerializeProtoToJniByteArray(env, reset_result_proto);
 }
 
+JNIEXPORT jbyteArray JNICALL
+Java_com_google_android_icing_IcingSearchEngineImpl_nativeClearAndDestroy(
+    JNIEnv* env, jclass clazz, jobject object) {
+  icing::lib::IcingSearchEngine* icing =
+      GetIcingSearchEnginePointer(env, object);
+
+  icing::lib::ResetResultProto clear_and_destroy_proto =
+      icing->ClearAndDestroy();
+
+  return SerializeProtoToJniByteArray(env, clear_and_destroy_proto);
+}
+
 jbyteArray nativeSearchSuggestions(JNIEnv* env, jclass clazz, jobject object,
                                    jbyteArray suggestion_spec_bytes) {
   icing::lib::IcingSearchEngine* icing =
@@ -623,6 +733,9 @@
       {"nativeSetSchema",
        "(Lcom/google/android/icing/IcingSearchEngineImpl;[BZ)[B",
        reinterpret_cast<void*>(nativeSetSchema)},
+      {"nativeSetSchemaWithRequestProto",
+       "(Lcom/google/android/icing/IcingSearchEngineImpl;[B)[B",
+       reinterpret_cast<void*>(nativeSetSchemaWithRequestProto)},
       {"nativeGetSchema",
        "(Lcom/google/android/icing/IcingSearchEngineImpl;)[B",
        reinterpret_cast<void*>(nativeGetSchema)},
@@ -634,15 +747,16 @@
        reinterpret_cast<void*>(nativeGetSchemaType)},
       {"nativePut", "(Lcom/google/android/icing/IcingSearchEngineImpl;[B)[B",
        reinterpret_cast<void*>(nativePut)},
-      // TODO(b/394875109): uncomment when Jetpack library is updated with this
-      // change and syned to google3.
-      // {"nativeBatchPut",
-      //  "(Lcom/google/android/icing/IcingSearchEngineImpl;[B)[B",
-      //  reinterpret_cast<void*>(nativeBatchPut)},
+      {"nativeBatchPut",
+       "(Lcom/google/android/icing/IcingSearchEngineImpl;[B)[B",
+       reinterpret_cast<void*>(nativeBatchPut)},
       {"nativeGet",
        "(Lcom/google/android/icing/IcingSearchEngineImpl;Ljava/lang/"
        "String;Ljava/lang/String;[B)[B",
        reinterpret_cast<void*>(nativeGet)},
+      {"nativeBatchGet",
+       "(Lcom/google/android/icing/IcingSearchEngineImpl;[B)[B",
+       reinterpret_cast<void*>(nativeBatchGet)},
       {"nativeReportUsage",
        "(Lcom/google/android/icing/IcingSearchEngineImpl;[B)[B",
        reinterpret_cast<void*>(nativeReportUsage)},
diff --git a/icing/join/delete-propagation-handler.cc b/icing/join/delete-propagation-handler.cc
new file mode 100644
index 0000000..b2943d4
--- /dev/null
+++ b/icing/join/delete-propagation-handler.cc
@@ -0,0 +1,161 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/join/delete-propagation-handler.h"
+
+#include <cstdint>
+#include <deque>
+#include <optional>
+#include <queue>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/absl_ports/canonical_errors.h"
+#include "icing/join/document-join-id-pair.h"
+#include "icing/join/qualified-id-join-index.h"
+#include "icing/schema/joinable-property.h"
+#include "icing/schema/schema-store.h"
+#include "icing/store/document-filter-data.h"
+#include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
+#include "icing/util/logging.h"
+#include "icing/util/status-macros.h"
+
+namespace icing {
+namespace lib {
+
+/* static */ libtextclassifier3::StatusOr<DeletePropagationHandler>
+DeletePropagationHandler::Create(
+    const SchemaStore* schema_store,
+    const QualifiedIdJoinIndex* qualified_id_join_index,
+    DocumentStore* document_store, int64_t current_time_ms) {
+  ICING_RETURN_ERROR_IF_NULL(schema_store);
+  ICING_RETURN_ERROR_IF_NULL(qualified_id_join_index);
+  ICING_RETURN_ERROR_IF_NULL(document_store);
+
+  if (qualified_id_join_index->version() !=
+      QualifiedIdJoinIndex::Version::kV3) {
+    return absl_ports::FailedPreconditionError(
+        "Delete propagation is enabled but qualified id join index v3 is not "
+        "used.");
+  }
+
+  return DeletePropagationHandler(schema_store, qualified_id_join_index,
+                                  document_store, current_time_ms);
+}
+
+libtextclassifier3::StatusOr<std::vector<DocumentStore::DocumentMetadata>>
+DeletePropagationHandler::Handle(
+    const std::unordered_set<DocumentId>& parent_doc_ids) {
+  ICING_ASSIGN_OR_RETURN(
+      std::unordered_set<DocumentId> propagated_child_doc_ids,
+      GetPropagatedChildDocumentIds(parent_doc_ids));
+
+  // Delete all propagated child documents.
+  std::vector<DocumentStore::DocumentMetadata> deleted_doc_metadata_list;
+  for (DocumentId child_doc_id : propagated_child_doc_ids) {
+    auto deleted_doc_metadata_or = document_store_.ForceDelete(child_doc_id);
+    if (!deleted_doc_metadata_or.ok()) {
+      if (absl_ports::IsNotFound(deleted_doc_metadata_or.status())) {
+        // The child document has already been deleted or expired, so skip the
+        // error. This should not happen, but let's check and skip it just in
+        // case.
+        continue;
+      }
+
+      // Real error.
+      return std::move(deleted_doc_metadata_or).status();
+    }
+    deleted_doc_metadata_list.push_back(
+        std::move(deleted_doc_metadata_or).ValueOrDie());
+  }
+
+  return deleted_doc_metadata_list;
+}
+
+libtextclassifier3::StatusOr<std::unordered_set<DocumentId>>
+DeletePropagationHandler::GetPropagatedChildDocumentIds(
+    const std::unordered_set<DocumentId>& parent_doc_ids) {
+  std::unordered_set<DocumentId> propagated_child_doc_ids;
+
+  // BFS traverse to find all propagated child documents.
+  std::queue<DocumentId> que(
+      std::deque(parent_doc_ids.begin(), parent_doc_ids.end()));
+  while (!que.empty()) {
+    DocumentId doc_id_to_expand = que.front();
+    que.pop();
+
+    ICING_ASSIGN_OR_RETURN(
+        QualifiedIdJoinIndex::DocumentJoinIdPairArrayView
+            child_join_id_pairs_array_view,
+        qualified_id_join_index_.GetDocumentJoinIdPairArrayView(
+            doc_id_to_expand));
+    for (const DocumentJoinIdPair& child_join_id_pair :
+         child_join_id_pairs_array_view) {
+      if (propagated_child_doc_ids.find(child_join_id_pair.document_id()) !=
+              propagated_child_doc_ids.end() ||
+          parent_doc_ids.find(child_join_id_pair.document_id()) !=
+              parent_doc_ids.end()) {
+        // Already added into the propagated set or in the parent set (happens
+        // only when there is a cycle back to the parent or traversed document
+        // in the join relation). Skip it.
+        continue;
+      }
+
+      // Get DocumentFilterData of the child document to look up its schema type
+      // id.
+      // - Skip if the child document has been deleted, since delete propagation
+      //   should've been done to all its children when deleting it previously.
+      // - Otherwise, we have to handle this child document and propagate to the
+      //   grandchildren when it is alive OR expired but not deleted.
+      std::optional<DocumentFilterData> child_filter_data =
+          document_store_.GetNonDeletedDocumentFilterData(
+              child_join_id_pair.document_id());
+      if (!child_filter_data) {
+        // The child document has been deleted. Skip.
+        continue;
+      }
+
+      auto metadata_or = schema_store_.GetJoinablePropertyMetadata(
+          child_filter_data->schema_type_id(),
+          child_join_id_pair.joinable_property_id());
+      if (!metadata_or.ok() || metadata_or.ValueOrDie() == nullptr) {
+        // This shouldn't happen because we've validated it during indexing and
+        // only put valid DocumentJoinIdPair into qualified id join index.
+        // Log and skip it.
+        ICING_LOG(ERROR) << "Failed to get metadata for schema type id "
+                         << child_filter_data->schema_type_id()
+                         << ", joinable property id "
+                         << static_cast<int>(
+                                child_join_id_pair.joinable_property_id());
+        continue;
+      }
+      const JoinablePropertyMetadata* metadata = metadata_or.ValueOrDie();
+
+      if (metadata->value_type == JoinableConfig::ValueType::QUALIFIED_ID &&
+          metadata->delete_propagation_type ==
+              JoinableConfig::DeletePropagationType::PROPAGATE_FROM) {
+        propagated_child_doc_ids.insert(child_join_id_pair.document_id());
+        que.push(child_join_id_pair.document_id());
+      }
+    }
+  }
+
+  return propagated_child_doc_ids;
+}
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/join/delete-propagation-handler.h b/icing/join/delete-propagation-handler.h
new file mode 100644
index 0000000..c8d3394
--- /dev/null
+++ b/icing/join/delete-propagation-handler.h
@@ -0,0 +1,85 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef ICING_JOIN_DELETE_PROPAGATION_HANDLER_H_
+#define ICING_JOIN_DELETE_PROPAGATION_HANDLER_H_
+
+#include <cstdint>
+#include <unordered_set>
+#include <vector>
+
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/join/qualified-id-join-index.h"
+#include "icing/schema/schema-store.h"
+#include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
+
+namespace icing {
+namespace lib {
+
+// A class to handle delete propagation related logics.
+class DeletePropagationHandler {
+ public:
+  // Creates a DeletePropagationHandler.
+  //
+  // Returns:
+  //   - DeletePropagationHandler on success.
+  //   - FAILED_PRECONDITION_ERROR if any pointer is nullptr or qualified id
+  //     join index version is not v3.
+  static libtextclassifier3::StatusOr<DeletePropagationHandler> Create(
+      const SchemaStore* schema_store,
+      const QualifiedIdJoinIndex* qualified_id_join_index,
+      DocumentStore* document_store, int64_t current_time_ms);
+
+  // Handles delete propagation for the given parent document ids.
+  //
+  // Note: this function DOES NOT handle parent_doc_ids' deletion. Instead, it
+  //   only deletes the propagated child documents.
+  //
+  // Returns:
+  //   - A vector of deleted child document metadata on success.
+  //   - INTERNAL_ERROR on any I/O errors.
+  libtextclassifier3::StatusOr<std::vector<DocumentStore::DocumentMetadata>>
+  Handle(const std::unordered_set<DocumentId>& parent_doc_ids);
+
+ private:
+  explicit DeletePropagationHandler(
+      const SchemaStore* schema_store,
+      const QualifiedIdJoinIndex* qualified_id_join_index,
+      DocumentStore* document_store, int64_t current_time_ms)
+      : schema_store_(*schema_store),
+        qualified_id_join_index_(*qualified_id_join_index),
+        document_store_(*document_store),
+        current_time_ms_(current_time_ms) {}
+
+  // Helper function to get all child document ids propagated from the given
+  // parent document ids via join relations with delete propagation enabled.
+  //
+  // Returns:
+  //   - A set of propagated child document ids on success.
+  //   - INTERNAL_ERROR on any I/O errors.
+  libtextclassifier3::StatusOr<std::unordered_set<DocumentId>>
+  GetPropagatedChildDocumentIds(
+      const std::unordered_set<DocumentId>& parent_doc_ids);
+
+  const SchemaStore& schema_store_;                      // Does not own.
+  const QualifiedIdJoinIndex& qualified_id_join_index_;  // Does not own.
+  DocumentStore& document_store_;                        // Does not own.
+  int64_t current_time_ms_;
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_JOIN_DELETE_PROPAGATION_HANDLER_H_
diff --git a/icing/join/delete-propagation-handler_test.cc b/icing/join/delete-propagation-handler_test.cc
new file mode 100644
index 0000000..1746937
--- /dev/null
+++ b/icing/join/delete-propagation-handler_test.cc
@@ -0,0 +1,935 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/join/delete-propagation-handler.h"
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <unordered_set>
+#include <utility>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "icing/document-builder.h"
+#include "icing/feature-flags.h"
+#include "icing/file/filesystem.h"
+#include "icing/file/portable-file-backed-proto-log.h"
+#include "icing/join/document-join-id-pair.h"
+#include "icing/join/qualified-id-join-index-impl-v2.h"
+#include "icing/join/qualified-id-join-index-impl-v3.h"
+#include "icing/join/qualified-id-join-index.h"
+#include "icing/join/qualified-id-join-indexing-handler.h"
+#include "icing/portable/gzip_stream.h"
+#include "icing/portable/platform.h"
+#include "icing/proto/document.pb.h"
+#include "icing/proto/document_wrapper.pb.h"
+#include "icing/proto/schema.pb.h"
+#include "icing/proto/scoring.pb.h"
+#include "icing/proto/search.pb.h"
+#include "icing/schema-builder.h"
+#include "icing/schema/schema-store.h"
+#include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
+#include "icing/testing/common-matchers.h"
+#include "icing/testing/fake-clock.h"
+#include "icing/testing/test-data.h"
+#include "icing/testing/test-feature-flags.h"
+#include "icing/testing/tmp-directory.h"
+#include "icing/tokenization/language-segmenter-factory.h"
+#include "icing/tokenization/language-segmenter.h"
+#include "icing/util/icu-data-file-helper.h"
+#include "icing/util/status-macros.h"
+#include "icing/util/tokenized-document.h"
+#include "unicode/uloc.h"
+
+namespace icing {
+namespace lib {
+
+namespace {
+
+using ::testing::ElementsAre;
+using ::testing::Eq;
+using ::testing::HasSubstr;
+using ::testing::IsEmpty;
+using ::testing::IsTrue;
+using ::testing::Ne;
+using ::testing::UnorderedElementsAre;
+
+class DeletePropagationHandlerTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+    fake_clock_.SetSystemTimeMilliseconds(123);
+
+    test_dir_ = GetTestTempDir() + "/icing_delete_propagation_handler_test";
+    ASSERT_THAT(filesystem_.CreateDirectoryRecursively(test_dir_.c_str()),
+                IsTrue());
+
+    schema_store_dir_ = test_dir_ + "/schema_store";
+    doc_store_dir_ = test_dir_ + "/doc_store";
+    qualified_id_join_index_dir_ = test_dir_ + "/qualified_id_join_index";
+
+    if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
+      ICING_ASSERT_OK(
+          // File generated via icu_data_file rule in //icing/BUILD.
+          icu_data_file_helper::SetUpIcuDataFile(
+              GetTestFilePath("icing/icu.dat")));
+    }
+
+    language_segmenter_factory::SegmenterOptions options(ULOC_US);
+    ICING_ASSERT_OK_AND_ASSIGN(
+        lang_segmenter_,
+        language_segmenter_factory::Create(std::move(options)));
+
+    ASSERT_THAT(
+        filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str()),
+        IsTrue());
+    ICING_ASSERT_OK_AND_ASSIGN(
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
+
+    SchemaProto schema =
+        SchemaBuilder()
+            .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+                PropertyConfigBuilder()
+                    .SetName("Name")
+                    .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                    .SetCardinality(CARDINALITY_OPTIONAL)))
+            .AddType(
+                SchemaTypeConfigBuilder()
+                    .SetType("Email")
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("subject")
+                                     .SetDataTypeString(TERM_MATCH_EXACT,
+                                                        TOKENIZER_PLAIN)
+                                     .SetCardinality(CARDINALITY_OPTIONAL))
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("sender")
+                                     .SetDataTypeJoinableString(
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                     .SetCardinality(CARDINALITY_OPTIONAL)))
+            .AddType(
+                SchemaTypeConfigBuilder()
+                    .SetType("Message")
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("content")
+                                     .SetDataTypeString(TERM_MATCH_EXACT,
+                                                        TOKENIZER_PLAIN)
+                                     .SetCardinality(CARDINALITY_OPTIONAL))
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("receiver")
+                                     .SetDataTypeJoinableString(
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                         DELETE_PROPAGATION_TYPE_NONE)
+                                     .SetCardinality(CARDINALITY_OPTIONAL))
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("reporter")
+                                     .SetDataTypeJoinableString(
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                         DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                     .SetCardinality(CARDINALITY_OPTIONAL))
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("sender")
+                                     .SetDataTypeJoinableString(
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                         DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                     .SetCardinality(CARDINALITY_OPTIONAL)))
+            .AddType(
+                SchemaTypeConfigBuilder()
+                    .SetType("Label")
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("text")
+                                     .SetDataTypeString(TERM_MATCH_EXACT,
+                                                        TOKENIZER_PLAIN)
+                                     .SetCardinality(CARDINALITY_OPTIONAL))
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("object")
+                                     .SetDataTypeJoinableString(
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                         DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                     .SetCardinality(CARDINALITY_OPTIONAL))
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("softLink")
+                                     .SetDataTypeJoinableString(
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                         DELETE_PROPAGATION_TYPE_NONE)
+                                     .SetCardinality(CARDINALITY_OPTIONAL)))
+
+            .Build();
+    ASSERT_THAT(schema_store_->SetSchema(
+                    schema, /*ignore_errors_and_delete_documents=*/false),
+                IsOk());
+
+    ASSERT_THAT(filesystem_.CreateDirectoryRecursively(doc_store_dir_.c_str()),
+                IsTrue());
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::CreateResult create_result,
+        DocumentStore::Create(
+            &filesystem_, doc_store_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
+    doc_store_ = std::move(create_result.document_store);
+
+    ICING_ASSERT_OK_AND_ASSIGN(
+        qualified_id_join_index_,
+        QualifiedIdJoinIndexImplV3::Create(
+            filesystem_, qualified_id_join_index_dir_, *feature_flags_));
+  }
+
+  void TearDown() override {
+    qualified_id_join_index_.reset();
+    doc_store_.reset();
+    schema_store_.reset();
+    lang_segmenter_.reset();
+
+    filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
+  }
+
+  libtextclassifier3::StatusOr<DocumentId> PutAndIndexDocument(
+      DocumentProto document) {
+    ICING_ASSIGN_OR_RETURN(
+        TokenizedDocument tokenized_document,
+        TokenizedDocument::Create(
+            schema_store_.get(), lang_segmenter_.get(),
+            /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+            std::move(document)));
+    ICING_ASSIGN_OR_RETURN(
+        DocumentStore::PutResult put_result,
+        doc_store_->Put(tokenized_document.document_wrapper()));
+
+    ICING_ASSIGN_OR_RETURN(
+        std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
+        QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
+                                               qualified_id_join_index_.get(),
+                                               feature_flags_.get()));
+    ICING_RETURN_IF_ERROR(
+        handler->Handle(tokenized_document, put_result.new_document_id,
+                        put_result.old_document_id, /*recovery_mode=*/false,
+                        /*put_document_stats=*/nullptr));
+    return put_result.new_document_id;
+  }
+
+  std::unique_ptr<FeatureFlags> feature_flags_;
+  Filesystem filesystem_;
+  std::string test_dir_;
+  std::string schema_store_dir_;
+  std::string doc_store_dir_;
+  std::string qualified_id_join_index_dir_;
+
+  std::unique_ptr<LanguageSegmenter> lang_segmenter_;
+  std::unique_ptr<SchemaStore> schema_store_;
+  std::unique_ptr<DocumentStore> doc_store_;
+  std::unique_ptr<QualifiedIdJoinIndex> qualified_id_join_index_;
+
+  FakeClock fake_clock_;
+};
+
+TEST_F(DeletePropagationHandlerTest, Create_shouldFailWithNullptr) {
+  EXPECT_THAT(DeletePropagationHandler::Create(
+                  /*schema_store=*/nullptr, qualified_id_join_index_.get(),
+                  doc_store_.get(), fake_clock_.GetSystemTimeMilliseconds()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+
+  EXPECT_THAT(DeletePropagationHandler::Create(
+                  schema_store_.get(), /*qualified_id_join_index=*/nullptr,
+                  doc_store_.get(), fake_clock_.GetSystemTimeMilliseconds()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+
+  EXPECT_THAT(
+      DeletePropagationHandler::Create(
+          schema_store_.get(), qualified_id_join_index_.get(),
+          /*document_store=*/nullptr, fake_clock_.GetSystemTimeMilliseconds()),
+      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+}
+
+TEST_F(DeletePropagationHandlerTest,
+       Create_shouldFailWithJoinIndexVersionNotV3) {
+  std::string join_index_v2_dir = test_dir_ + "/join_index_v2";
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<QualifiedIdJoinIndex> join_index_v2,
+      QualifiedIdJoinIndexImplV2::Create(filesystem_,
+                                         std::move(join_index_v2_dir),
+                                         /*pre_mapping_fbv=*/false));
+  EXPECT_THAT(DeletePropagationHandler::Create(
+                  schema_store_.get(), join_index_v2.get(), doc_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION,
+                       HasSubstr("Delete propagation is enabled but qualified "
+                                 "id join index v3 is not used")));
+}
+
+TEST_F(DeletePropagationHandlerTest, Handle) {
+  DocumentProto person = DocumentBuilder()
+                             .SetKey("pkg$db/namespace", "person")
+                             .SetSchema("Person")
+                             .AddStringProperty("Name", "Alice")
+                             .Build();
+
+  // "sender" (joinable property id 2) has
+  // DELETE_PROPAGATION_TYPE_PROPAGATE_FROM.
+  DocumentProto message =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "message")
+          .SetSchema("Message")
+          .AddStringProperty("content", "test content")
+          .AddStringProperty("sender", "pkg$db/namespace#person")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person_doc_id,
+                             PutAndIndexDocument(person));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message_doc_id,
+                             PutAndIndexDocument(message));
+  // Sanity check.
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(person_doc_id),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(message_doc_id, /*joinable_property_id=*/2))));
+
+  // Deleting the parent document should propagate the delete to its child
+  // document via the joinable property with
+  // DELETE_PROPAGATION_TYPE_PROPAGATE_FROM.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DeletePropagationHandler delete_propagation_handler,
+      DeletePropagationHandler::Create(
+          schema_store_.get(), qualified_id_join_index_.get(), doc_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(
+      delete_propagation_handler.Handle(/*parent_doc_ids=*/{person_doc_id}),
+      IsOkAndHolds(UnorderedElementsAre(EqualsDocumentMetadata(
+          "Message", "pkg$db/namespace", "message", message_doc_id))));
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(message_doc_id),
+              Eq(std::nullopt));
+}
+
+TEST_F(DeletePropagationHandlerTest,
+       Handle_shouldNotPropagateToChildDocumentsWithPropagateDeleteDisabled) {
+  DocumentProto person = DocumentBuilder()
+                             .SetKey("pkg$db/namespace", "person")
+                             .SetSchema("Person")
+                             .AddStringProperty("Name", "Alice")
+                             .Build();
+
+  // "receiver" (joinable property id 0) has DELETE_PROPAGATION_TYPE_NONE.
+  DocumentProto message =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "message")
+          .SetSchema("Message")
+          .AddStringProperty("content", "test content")
+          .AddStringProperty("receiver", "pkg$db/namespace#person")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person_doc_id,
+                             PutAndIndexDocument(person));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message_doc_id,
+                             PutAndIndexDocument(message));
+  // Sanity check.
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(person_doc_id),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(message_doc_id, /*joinable_property_id=*/0))));
+
+  // Deleting the parent document should not propagate the delete to its child
+  // document via the joinable property with DELETE_PROPAGATION_TYPE_NONE.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DeletePropagationHandler delete_propagation_handler,
+      DeletePropagationHandler::Create(
+          schema_store_.get(), qualified_id_join_index_.get(), doc_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(
+      delete_propagation_handler.Handle(/*parent_doc_ids=*/{person_doc_id}),
+      IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(message_doc_id),
+              Ne(std::nullopt));
+}
+
+TEST_F(DeletePropagationHandlerTest,
+       Handle_shouldNotPropagateToNonJoinableChildDocuments) {
+  DocumentProto person = DocumentBuilder()
+                             .SetKey("pkg$db/namespace", "person")
+                             .SetSchema("Person")
+                             .AddStringProperty("Name", "Alice")
+                             .Build();
+
+  // Put person's qualified id string in a non-joinable property.
+  DocumentProto message =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "message")
+          .SetSchema("Message")
+          .AddStringProperty("content", "pkg$db/namespace#person")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person_doc_id,
+                             PutAndIndexDocument(person));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message_doc_id,
+                             PutAndIndexDocument(message));
+  // Sanity check.
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(person_doc_id),
+      IsOkAndHolds(IsEmpty()));
+
+  // Deleting the parent document should not propagate the delete to
+  // non-joinable child documents.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DeletePropagationHandler delete_propagation_handler,
+      DeletePropagationHandler::Create(
+          schema_store_.get(), qualified_id_join_index_.get(), doc_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(
+      delete_propagation_handler.Handle(/*parent_doc_ids=*/{person_doc_id}),
+      IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(message_doc_id),
+              Ne(std::nullopt));
+}
+
+TEST_F(DeletePropagationHandlerTest, Handle_propagateViaMultipleProperties) {
+  DocumentProto person = DocumentBuilder()
+                             .SetKey("pkg$db/namespace", "person")
+                             .SetSchema("Person")
+                             .AddStringProperty("Name", "Alice")
+                             .Build();
+
+  // - "sender" (joinable property id 2) has
+  //   DELETE_PROPAGATION_TYPE_PROPAGATE_FROM.
+  // - "receiver" (joinable property id 0) has DELETE_PROPAGATION_TYPE_NONE.
+  DocumentProto message =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "message")
+          .SetSchema("Message")
+          .AddStringProperty("content", "test content")
+          .AddStringProperty("sender", "pkg$db/namespace#person")
+          .AddStringProperty("receiver", "pkg$db/namespace#person")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person_doc_id,
+                             PutAndIndexDocument(person));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message_doc_id,
+                             PutAndIndexDocument(message));
+  // Sanity check.
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(person_doc_id),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(message_doc_id, /*joinable_property_id=*/0),
+          DocumentJoinIdPair(message_doc_id, /*joinable_property_id=*/2))));
+
+  // Deleting the parent document should propagate the delete to its child
+  // document when there is at least one property with
+  // DELETE_PROPAGATION_TYPE_PROPAGATE_FROM.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DeletePropagationHandler delete_propagation_handler,
+      DeletePropagationHandler::Create(
+          schema_store_.get(), qualified_id_join_index_.get(), doc_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(
+      delete_propagation_handler.Handle(/*parent_doc_ids=*/{person_doc_id}),
+      IsOkAndHolds(UnorderedElementsAre(EqualsDocumentMetadata(
+          "Message", "pkg$db/namespace", "message", message_doc_id))));
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(message_doc_id),
+              Eq(std::nullopt));
+}
+
+TEST_F(DeletePropagationHandlerTest, Handle_propagateToMultipleChildren) {
+  DocumentProto person = DocumentBuilder()
+                             .SetKey("pkg$db/namespace", "person")
+                             .SetSchema("Person")
+                             .AddStringProperty("Name", "Alice")
+                             .Build();
+
+  // "sender" (joinable property id 2) has
+  // DELETE_PROPAGATION_TYPE_PROPAGATE_FROM.
+  DocumentProto message1 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "message1")
+          .SetSchema("Message")
+          .AddStringProperty("content", "test content")
+          .AddStringProperty("sender", "pkg$db/namespace#person")
+          .Build();
+  DocumentProto message2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "message2")
+          .SetSchema("Message")
+          .AddStringProperty("content", "test content")
+          .AddStringProperty("sender", "pkg$db/namespace#person")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person_doc_id,
+                             PutAndIndexDocument(person));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message1_doc_id,
+                             PutAndIndexDocument(message1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message2_doc_id,
+                             PutAndIndexDocument(message2));
+  // Sanity check.
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(person_doc_id),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(message1_doc_id, /*joinable_property_id=*/2),
+          DocumentJoinIdPair(message2_doc_id, /*joinable_property_id=*/2))));
+
+  // Deleting the parent document should propagate the delete to all of its
+  // child documents via the joinable property with
+  // DELETE_PROPAGATION_TYPE_PROPAGATE_FROM.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DeletePropagationHandler delete_propagation_handler,
+      DeletePropagationHandler::Create(
+          schema_store_.get(), qualified_id_join_index_.get(), doc_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(
+      delete_propagation_handler.Handle(/*parent_doc_ids=*/{person_doc_id}),
+      IsOkAndHolds(UnorderedElementsAre(
+          EqualsDocumentMetadata("Message", "pkg$db/namespace", "message1",
+                                 message1_doc_id),
+          EqualsDocumentMetadata("Message", "pkg$db/namespace", "message2",
+                                 message2_doc_id))));
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(message1_doc_id),
+              Eq(std::nullopt));
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(message2_doc_id),
+              Eq(std::nullopt));
+}
+
+TEST_F(DeletePropagationHandlerTest, Handle_propagateFromMultipleProperties) {
+  DocumentProto person1 = DocumentBuilder()
+                              .SetKey("pkg$db/namespace", "person1")
+                              .SetSchema("Person")
+                              .AddStringProperty("Name", "Alice")
+                              .Build();
+  DocumentProto person2 = DocumentBuilder()
+                              .SetKey("pkg$db/namespace", "person2")
+                              .SetSchema("Person")
+                              .AddStringProperty("Name", "Bob")
+                              .Build();
+
+  // "sender" (joinable property id 2) and "reporter" (joinable property id 1)
+  // have DELETE_PROPAGATION_TYPE_PROPAGATE_FROM.
+  DocumentProto message =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "message")
+          .SetSchema("Message")
+          .AddStringProperty("content", "test content")
+          .AddStringProperty("sender", "pkg$db/namespace#person1")
+          .AddStringProperty("reporter", "pkg$db/namespace#person2")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person1_doc_id,
+                             PutAndIndexDocument(person1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person2_doc_id,
+                             PutAndIndexDocument(person2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message_doc_id,
+                             PutAndIndexDocument(message));
+  // Sanity check.
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(person1_doc_id),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(message_doc_id, /*joinable_property_id=*/2))));
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(person2_doc_id),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(message_doc_id, /*joinable_property_id=*/1))));
+
+  // message document should be propagated to be deleted from both person1 and
+  // person2 via "sender" and "reporter" properties respectively.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DeletePropagationHandler delete_propagation_handler,
+      DeletePropagationHandler::Create(
+          schema_store_.get(), qualified_id_join_index_.get(), doc_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(delete_propagation_handler.Handle(
+                  /*parent_doc_ids=*/{person1_doc_id, person2_doc_id}),
+              IsOkAndHolds(UnorderedElementsAre(EqualsDocumentMetadata(
+                  "Message", "pkg$db/namespace", "message", message_doc_id))));
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(message_doc_id),
+              Eq(std::nullopt));
+}
+
+TEST_F(DeletePropagationHandlerTest, Handle_propagateToGrandChildren) {
+  // Create the following relations:
+  //
+  //                         ("object") - label1
+  //                        /
+  //             message1 <-
+  //            /           \
+  //      ("sender")         ("softLink") - label2
+  //          /
+  // person <-
+  //          \
+  //      ("receiver")       ("object") - label3
+  //            \           /
+  //             message2 <-
+  //                        \
+  //                         ("softLink") - label4
+  //
+  // Note: "sender" and "object" have DELETE_PROPAGATION_TYPE_PROPAGATE_FROM,
+  //       while "receiver" and "softLink" have DELETE_PROPAGATION_TYPE_NONE.
+  DocumentProto person = DocumentBuilder()
+                             .SetKey("pkg$db/namespace", "person")
+                             .SetSchema("Person")
+                             .AddStringProperty("Name", "Alice")
+                             .Build();
+
+  DocumentProto message1 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "message1")
+          .SetSchema("Message")
+          .AddStringProperty("content", "test content")
+          .AddStringProperty("sender", "pkg$db/namespace#person")
+          .Build();
+  DocumentProto message2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "message2")
+          .SetSchema("Message")
+          .AddStringProperty("content", "test content")
+          .AddStringProperty("receiver", "pkg$db/namespace#person")
+          .Build();
+
+  DocumentProto label1 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "label1")
+          .SetSchema("Label")
+          .AddStringProperty("text", " label1")
+          .AddStringProperty("object", "pkg$db/namespace#message1")
+          .Build();
+  DocumentProto label2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "label2")
+          .SetSchema("Label")
+          .AddStringProperty("text", " label2")
+          .AddStringProperty("softLink", "pkg$db/namespace#message1")
+          .Build();
+  DocumentProto label3 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "label3")
+          .SetSchema("Label")
+          .AddStringProperty("text", " label3")
+          .AddStringProperty("object", "pkg$db/namespace#message2")
+          .Build();
+  DocumentProto label4 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "label4")
+          .SetSchema("Label")
+          .AddStringProperty("text", " label4")
+          .AddStringProperty("softLink", "pkg$db/namespace#message2")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person_doc_id,
+                             PutAndIndexDocument(person));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message1_doc_id,
+                             PutAndIndexDocument(message1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message2_doc_id,
+                             PutAndIndexDocument(message2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label1_doc_id,
+                             PutAndIndexDocument(label1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label2_doc_id,
+                             PutAndIndexDocument(label2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label3_doc_id,
+                             PutAndIndexDocument(label3));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label4_doc_id,
+                             PutAndIndexDocument(label4));
+
+  // - For children with type "Message", only message1 should be propagated to
+  //   be deleted.
+  // - For grand children with type "Label", only label1 should be propagated to
+  //   be deleted.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DeletePropagationHandler delete_propagation_handler,
+      DeletePropagationHandler::Create(
+          schema_store_.get(), qualified_id_join_index_.get(), doc_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(
+      delete_propagation_handler.Handle(/*parent_doc_ids=*/{person_doc_id}),
+      IsOkAndHolds(UnorderedElementsAre(
+          EqualsDocumentMetadata("Message", "pkg$db/namespace", "message1",
+                                 message1_doc_id),
+          EqualsDocumentMetadata("Label", "pkg$db/namespace", "label1",
+                                 label1_doc_id))));
+
+  // message1 and label1 should be deleted, while message2 and label2/3/4
+  // should not be deleted.
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(message1_doc_id),
+              Eq(std::nullopt));
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(label1_doc_id),
+              Eq(std::nullopt));
+
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(message2_doc_id),
+              Ne(std::nullopt));
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(label2_doc_id),
+              Ne(std::nullopt));
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(label3_doc_id),
+              Ne(std::nullopt));
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(label4_doc_id),
+              Ne(std::nullopt));
+}
+
+TEST_F(DeletePropagationHandlerTest, Handle_cycleReference) {
+  // Create the following relations:
+  //
+  // label1 <- label2 <- label3
+  //   |                   ^
+  //   |                   |
+  //   +-------------------+
+  DocumentProto label1 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "label1")
+          .SetSchema("Label")
+          .AddStringProperty("text", " label1")
+          .AddStringProperty("object", "pkg$db/namespace#label3")
+          .Build();
+  DocumentProto label2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "label2")
+          .SetSchema("Label")
+          .AddStringProperty("text", " label2")
+          .AddStringProperty("object", "pkg$db/namespace#label1")
+          .Build();
+  DocumentProto label3 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "label3")
+          .SetSchema("Label")
+          .AddStringProperty("text", " label3")
+          .AddStringProperty("object", "pkg$db/namespace#label2")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label1_doc_id,
+                             PutAndIndexDocument(label1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label2_doc_id,
+                             PutAndIndexDocument(label2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label3_doc_id,
+                             PutAndIndexDocument(label3));
+
+  // label1 should get children = [label2_doc_id].
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(label1_doc_id),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(label2_doc_id, /*joinable_property_id=*/0))));
+  // label2 should get children = [label3_doc_id].
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(label2_doc_id),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(label3_doc_id, /*joinable_property_id=*/0))));
+  // label3 should get children = [label1_doc_id]
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(label3_doc_id),
+      IsOkAndHolds(
+          ElementsAre(DocumentJoinIdPair(label1_doc_id,
+                                         /*joinable_property_id=*/0))));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DeletePropagationHandler delete_propagation_handler,
+      DeletePropagationHandler::Create(
+          schema_store_.get(), qualified_id_join_index_.get(), doc_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds()));
+
+  // Handle {label1_doc_id}:
+  // - Propagate to label2_doc_id from label1_doc_id.
+  // - Propagate to label3_doc_id from label2_doc_id.
+  // - When trying to propage label3_doc_id to its children =
+  //   [label1_doc_id]:
+  //   - label1_doc_id is already deleted, so it should not be propagated
+  //     again.
+  //   - There should be no infinite propagation loop.
+  EXPECT_THAT(
+      delete_propagation_handler.Handle(/*parent_doc_ids=*/{label1_doc_id}),
+      IsOkAndHolds(UnorderedElementsAre(
+          EqualsDocumentMetadata("Label", "pkg$db/namespace", "label2",
+                                 label2_doc_id),
+          EqualsDocumentMetadata("Label", "pkg$db/namespace", "label3",
+                                 label3_doc_id))));
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(label2_doc_id),
+              Eq(std::nullopt));
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(label3_doc_id),
+              Eq(std::nullopt));
+}
+
+TEST_F(DeletePropagationHandlerTest, Handle_selfCycleReference) {
+  DocumentProto label =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "label")
+          .SetSchema("Label")
+          .AddStringProperty("text", " label")
+          .AddStringProperty("object", "pkg$db/namespace#label")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label_doc_id,
+                             PutAndIndexDocument(label));
+
+  // Sanity check.
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(label_doc_id),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(label_doc_id, /*joinable_property_id=*/0))));
+
+  // Handle {label_doc_id}: should delete nothing since label_doc_id is already
+  // in the deleted set. Also there should be no infinite propagation loop.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DeletePropagationHandler delete_propagation_handler,
+      DeletePropagationHandler::Create(
+          schema_store_.get(), qualified_id_join_index_.get(), doc_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(
+      delete_propagation_handler.Handle(/*parent_doc_ids=*/{label_doc_id}),
+      IsOkAndHolds(IsEmpty()));
+}
+
+TEST_F(DeletePropagationHandlerTest, Handle_shouldPropagateToExpiredDocuments) {
+  constexpr int64_t kCreationTimestampMs = 1000;
+  constexpr int64_t kShortTtlMs = 1000;
+  constexpr int64_t kLongTtlMs = 10000;
+
+  // Create the following relations:
+  //
+  // label1 <- label2 <- label3
+  DocumentProto label1 = DocumentBuilder()
+                             .SetKey("pkg$db/namespace", "label1")
+                             .SetSchema("Label")
+                             .AddStringProperty("text", " label1")
+                             .SetCreationTimestampMs(kCreationTimestampMs)
+                             .SetTtlMs(kLongTtlMs)
+                             .Build();
+  DocumentProto label2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "label2")
+          .SetSchema("Label")
+          .AddStringProperty("text", " label2")
+          .AddStringProperty("object", "pkg$db/namespace#label1")
+          .SetCreationTimestampMs(kCreationTimestampMs)
+          .SetTtlMs(kShortTtlMs)
+          .Build();
+  DocumentProto label3 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "label3")
+          .SetSchema("Label")
+          .AddStringProperty("text", " label3")
+          .AddStringProperty("object", "pkg$db/namespace#label2")
+          .SetCreationTimestampMs(kCreationTimestampMs)
+          .SetTtlMs(kLongTtlMs)
+          .Build();
+
+  fake_clock_.SetSystemTimeMilliseconds(kCreationTimestampMs);
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label1_doc_id,
+                             PutAndIndexDocument(label1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label2_doc_id,
+                             PutAndIndexDocument(label2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label3_doc_id,
+                             PutAndIndexDocument(label3));
+
+  // Adjust the clock to expire label2.
+  int64_t current_time_ms = kCreationTimestampMs + kShortTtlMs + 100;
+  fake_clock_.SetSystemTimeMilliseconds(current_time_ms);
+
+  // Sanity check: label2 is expired (but not deleted), and label1 and label3
+  // are not expired.
+  ASSERT_THAT(
+      doc_store_->GetAliveDocumentFilterData(label1_doc_id, current_time_ms),
+      Ne(std::nullopt));
+  ASSERT_THAT(
+      doc_store_->GetAliveDocumentFilterData(label2_doc_id, current_time_ms),
+      Eq(std::nullopt));
+  ASSERT_THAT(doc_store_->GetNonDeletedDocumentFilterData(label2_doc_id),
+              Ne(std::nullopt));
+  ASSERT_THAT(
+      doc_store_->GetAliveDocumentFilterData(label3_doc_id, current_time_ms),
+      Ne(std::nullopt));
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(label1_doc_id),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(label2_doc_id, /*joinable_property_id=*/0))));
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(label2_doc_id),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(label3_doc_id, /*joinable_property_id=*/0))));
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(label3_doc_id),
+      IsOkAndHolds(IsEmpty()));
+
+  // Handle {label1_doc_id}: should still propagate to label2_doc_id and
+  // label3_doc_id even though label2_doc_id is expired.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DeletePropagationHandler delete_propagation_handler,
+      DeletePropagationHandler::Create(
+          schema_store_.get(), qualified_id_join_index_.get(), doc_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(
+      delete_propagation_handler.Handle(/*parent_doc_ids=*/{label1_doc_id}),
+      IsOkAndHolds(UnorderedElementsAre(
+          EqualsDocumentMetadata("Label", "pkg$db/namespace", "label2",
+                                 label2_doc_id),
+          EqualsDocumentMetadata("Label", "pkg$db/namespace", "label3",
+                                 label3_doc_id))));
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(label2_doc_id),
+              Eq(std::nullopt));
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(label3_doc_id),
+              Eq(std::nullopt));
+}
+
+TEST_F(DeletePropagationHandlerTest,
+       Handle_shouldNotPropagateToDeletedDocuments) {
+  // Create the following relations:
+  //
+  // label1 <- label2
+  DocumentProto label1 = DocumentBuilder()
+                             .SetKey("pkg$db/namespace", "label1")
+                             .SetSchema("Label")
+                             .AddStringProperty("text", " label1")
+                             .Build();
+  DocumentProto label2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "label2")
+          .SetSchema("Label")
+          .AddStringProperty("text", " label2")
+          .AddStringProperty("object", "pkg$db/namespace#label1")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label1_doc_id,
+                             PutAndIndexDocument(label1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label2_doc_id,
+                             PutAndIndexDocument(label2));
+
+  // Delete label2.
+  ICING_ASSERT_OK(doc_store_->Delete(label2_doc_id,
+                                     fake_clock_.GetSystemTimeMilliseconds()));
+
+  // Sanity check: label2 is deleted, but join data is still present.
+  ASSERT_THAT(doc_store_->GetNonDeletedDocumentFilterData(label2_doc_id),
+              Eq(std::nullopt));
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(label1_doc_id),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(label2_doc_id, /*joinable_property_id=*/0))));
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(label2_doc_id),
+      IsOkAndHolds(IsEmpty()));
+
+  // Handle {label1_doc_id}: should not propagate to label2_doc_id.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DeletePropagationHandler delete_propagation_handler,
+      DeletePropagationHandler::Create(
+          schema_store_.get(), qualified_id_join_index_.get(), doc_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(
+      delete_propagation_handler.Handle(/*parent_doc_ids=*/{label1_doc_id}),
+      IsOkAndHolds(IsEmpty()));
+}
+
+}  // namespace
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/join/document-dependency-processor.cc b/icing/join/document-dependency-processor.cc
new file mode 100644
index 0000000..8889734
--- /dev/null
+++ b/icing/join/document-dependency-processor.cc
@@ -0,0 +1,141 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/join/document-dependency-processor.h"
+
+#include <cstdint>
+#include <string_view>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/absl_ports/canonical_errors.h"
+#include "icing/absl_ports/str_cat.h"
+#include "icing/join/qualified-id.h"
+#include "icing/proto/document.pb.h"
+#include "icing/schema/joinable-property.h"
+#include "icing/store/document-store.h"
+#include "icing/util/status-macros.h"
+#include "icing/util/timestamp-util.h"
+#include "icing/util/tokenized-document.h"
+
+namespace icing {
+namespace lib {
+
+/* static */ libtextclassifier3::StatusOr<DocumentDependencyProcessor>
+DocumentDependencyProcessor::Create(
+    const DocumentStore* document_store,
+    const std::vector<TokenizedDocument>& batch_documents_to_add,
+    int64_t current_time_ms) {
+  ICING_RETURN_ERROR_IF_NULL(document_store);
+
+  std::unordered_map<QualifiedId, int, QualifiedId::Hasher>
+      qualified_id_to_batch_idx;
+  for (int i = 0; i < batch_documents_to_add.size(); ++i) {
+    const TokenizedDocument& tokenized_document = batch_documents_to_add[i];
+
+    // Ensure that the new document is not expired.
+    int64_t expiration_timestamp_ms =
+        timestamp_util::CalculateRawExpirationTimestampMs(
+            tokenized_document.document_wrapper()
+                .document()
+                .creation_timestamp_ms(),
+            tokenized_document.document_wrapper().document().ttl_ms());
+    if (expiration_timestamp_ms <= current_time_ms) {
+      return absl_ports::InvalidArgumentError("The new document is expired.");
+    }
+
+    QualifiedId qualified_id(
+        tokenized_document.document_wrapper().document().namespace_(),
+        tokenized_document.document_wrapper().document().uri());
+    qualified_id_to_batch_idx.insert({std::move(qualified_id), i});
+  }
+
+  return DocumentDependencyProcessor(document_store, batch_documents_to_add,
+                                     std::move(qualified_id_to_batch_idx),
+                                     current_time_ms);
+}
+
+libtextclassifier3::Status DocumentDependencyProcessor::Evaluate() {
+  // Validate the dependencies and construct the dependent graph.
+  for (int i = 0; i < batch_documents_to_add_.size(); ++i) {
+    const TokenizedDocument& tokenized_document = batch_documents_to_add_[i];
+
+    // Iterate through all qualified id joinable properties of the tokenized
+    // document.
+    for (const JoinableProperty<std::string_view>& dep_qualified_id_prop :
+         tokenized_document.qualified_id_join_properties()) {
+      if (dep_qualified_id_prop.metadata.delete_propagation_type ==
+          JoinableConfig::DeletePropagationType::NONE) {
+        // If delete propagation is NONE, then the referenced documents are
+        // not required to be present, so skip the check for this joinable
+        // property.
+        continue;
+      }
+
+      // Otherwise, the referenced documents are required to be present.
+      // For each of the qualified id string:
+      // - Validate and check it should match a document in either the same
+      //   batch of new documents to add or the document store.
+      // - Add the dependency document id (out of the batch) into result.
+      for (std::string_view dep_qualified_id_str :
+           dep_qualified_id_prop.values) {
+        ICING_RETURN_IF_ERROR(ValidateDependency(dep_qualified_id_str));
+      }
+    }
+  }
+  return libtextclassifier3::Status::OK;
+}
+
+libtextclassifier3::Status DocumentDependencyProcessor::ValidateDependency(
+    std::string_view dep_qualified_id_str) const {
+  if (dep_qualified_id_str.empty()) {
+    // Allow empty qualified id.
+    return libtextclassifier3::Status::OK;
+  }
+
+  // Attempt to parse the qualified id string.
+  auto dep_qualified_id_or = QualifiedId::Parse(dep_qualified_id_str);
+  if (!dep_qualified_id_or.ok()) {
+    // Incorrect format of qualified id string. Return INVALID_ARGUMENT_ERROR
+    // for unsatisfied dependency.
+    return absl_ports::InvalidArgumentError(absl_ports::StrCat(
+        "Invalid qualified id string: ", dep_qualified_id_str));
+  }
+  QualifiedId dep_qualified_id = std::move(dep_qualified_id_or).ValueOrDie();
+
+  // Case 1: check if the dependency document is in the same batch of new
+  //         documents.
+  auto itr = qualified_id_to_batch_idx_.find(dep_qualified_id);
+  if (itr != qualified_id_to_batch_idx_.end()) {
+    // We've already validated that the document in the batch is not expired, so
+    // we don't need to check it again here.
+    return libtextclassifier3::Status::OK;
+  }
+
+  // Case 2: check if the dependency document is alive in the document store.
+  if (!document_store_.IsDocumentAlive(dep_qualified_id.name_space(),
+                                       dep_qualified_id.uri(),
+                                       current_time_ms_)) {
+    return absl_ports::InvalidArgumentError(absl_ports::StrCat(
+        "A dependency document is not alive: ", dep_qualified_id_str));
+  }
+
+  return libtextclassifier3::Status::OK;
+}
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/join/document-dependency-processor.h b/icing/join/document-dependency-processor.h
new file mode 100644
index 0000000..01e63f8
--- /dev/null
+++ b/icing/join/document-dependency-processor.h
@@ -0,0 +1,100 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef ICING_JOIN_DOCUMENT_DEPENDENCY_PROCESSOR_H_
+#define ICING_JOIN_DOCUMENT_DEPENDENCY_PROCESSOR_H_
+
+#include <cstdint>
+#include <string_view>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/join/qualified-id.h"
+#include "icing/proto/document.pb.h"
+#include "icing/store/document-store.h"
+#include "icing/util/tokenized-document.h"
+
+namespace icing {
+namespace lib {
+
+// This class evaluates the dependency of the documents to be added. Currently,
+// dependencies are defined solely by delete propagation.
+class DocumentDependencyProcessor {
+ public:
+  // Creates a DocumentDependencyProcessor for the given batch of documents to
+  // add.
+  //
+  // Returns:
+  //   - A DocumentDependencyProcessor on success.
+  //   - INVALID_ARGUMENT_ERROR if any of the document in the batch is expired.
+  static libtextclassifier3::StatusOr<DocumentDependencyProcessor> Create(
+      const DocumentStore* document_store,
+      const std::vector<TokenizedDocument>& batch_documents_to_add,
+      int64_t current_time_ms);
+
+  // Evaluates the document dependencies. For each document in the batch, its
+  // dependencies (parent documents with delete propagation enabled in the
+  // schema) must be alive in either the same batch of new documents or the
+  // document store.
+  //
+  // Returns:
+  //   - OK on success.
+  //   - INVALID_ARGUMENT_ERROR if the validation fails, e.g. any of the
+  //     dependencies (referenced parent documents) are not present.
+  //   - Any error from document store or schema store.
+  libtextclassifier3::Status Evaluate();
+
+ private:
+  explicit DocumentDependencyProcessor(
+      const DocumentStore* document_store,
+      const std::vector<TokenizedDocument>& batch_documents_to_add,
+      std::unordered_map<QualifiedId, int, QualifiedId::Hasher>
+          qualified_id_to_batch_idx,
+      int64_t current_time_ms)
+      : document_store_(*document_store),
+        batch_documents_to_add_(batch_documents_to_add),
+        qualified_id_to_batch_idx_(std::move(qualified_id_to_batch_idx)),
+        current_time_ms_(current_time_ms) {}
+
+  // Helper function to validate a dependency's qualified id string:
+  // - Is valid or not. Note that empty qualified id string is allowed and will
+  //   be skipped.
+  // - Satisfies the dependency: matches a document in either the same batch of
+  //   new documents to add or the document store.
+  //
+  // Returns:
+  //   - OK on success.
+  //   - INVALID_ARGUMENT_ERROR if dep_qualified_id_str is invalid or the
+  //     document referenced by the qualified id is not present.
+  //   - Any error from document store.
+  libtextclassifier3::Status ValidateDependency(
+      std::string_view dep_qualified_id_str) const;
+
+  const DocumentStore& document_store_;
+  const std::vector<TokenizedDocument>& batch_documents_to_add_;
+
+  // A map for mapping qualified id to the index of batch_documents_to_add_.
+  std::unordered_map<QualifiedId, int, QualifiedId::Hasher>
+      qualified_id_to_batch_idx_;
+
+  int64_t current_time_ms_;
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_JOIN_DOCUMENT_DEPENDENCY_PROCESSOR_H_
diff --git a/icing/join/document-dependency-processor_test.cc b/icing/join/document-dependency-processor_test.cc
new file mode 100644
index 0000000..16243fd
--- /dev/null
+++ b/icing/join/document-dependency-processor_test.cc
@@ -0,0 +1,948 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/join/document-dependency-processor.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "icing/document-builder.h"
+#include "icing/feature-flags.h"
+#include "icing/file/filesystem.h"
+#include "icing/file/portable-file-backed-proto-log.h"
+#include "icing/portable/gzip_stream.h"
+#include "icing/portable/platform.h"
+#include "icing/proto/document.pb.h"
+#include "icing/proto/document_wrapper.pb.h"
+#include "icing/proto/schema.pb.h"
+#include "icing/proto/scoring.pb.h"
+#include "icing/proto/search.pb.h"
+#include "icing/schema-builder.h"
+#include "icing/schema/schema-store.h"
+#include "icing/store/document-store.h"
+#include "icing/testing/common-matchers.h"
+#include "icing/testing/fake-clock.h"
+#include "icing/testing/test-data.h"
+#include "icing/testing/test-feature-flags.h"
+#include "icing/testing/tmp-directory.h"
+#include "icing/tokenization/language-segmenter-factory.h"
+#include "icing/tokenization/language-segmenter.h"
+#include "icing/util/document-util.h"
+#include "icing/util/icu-data-file-helper.h"
+#include "icing/util/tokenized-document.h"
+#include "unicode/uloc.h"
+
+namespace icing {
+namespace lib {
+
+namespace {
+
+using ::testing::HasSubstr;
+using ::testing::IsTrue;
+
+class DocumentDependencyProcessorTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+    ASSERT_THAT(feature_flags_->enable_repeated_field_joins(), IsTrue());
+
+    test_dir_ = GetTestTempDir() + "/document_dependency_processor_test";
+    ASSERT_THAT(filesystem_.CreateDirectoryRecursively(test_dir_.c_str()),
+                IsTrue());
+
+    schema_store_dir_ = test_dir_ + "/schema_store";
+    doc_store_dir_ = test_dir_ + "/doc_store";
+
+    if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
+      ICING_ASSERT_OK(
+          // File generated via icu_data_file rule in //icing/BUILD.
+          icu_data_file_helper::SetUpIcuDataFile(
+              GetTestFilePath("icing/icu.dat")));
+    }
+
+    language_segmenter_factory::SegmenterOptions options(ULOC_US);
+    ICING_ASSERT_OK_AND_ASSIGN(
+        lang_segmenter_,
+        language_segmenter_factory::Create(std::move(options)));
+
+    ASSERT_THAT(
+        filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str()),
+        IsTrue());
+    ICING_ASSERT_OK_AND_ASSIGN(
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
+
+    SchemaProto schema =
+        SchemaBuilder()
+            .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+                PropertyConfigBuilder()
+                    .SetName("Name")
+                    .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                    .SetCardinality(CARDINALITY_OPTIONAL)))
+            .AddType(
+                SchemaTypeConfigBuilder()
+                    .SetType("Email")
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("receiver")
+                                     .SetDataTypeJoinableString(
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                         DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                     .SetCardinality(CARDINALITY_REPEATED))
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("sender")
+                                     .SetDataTypeJoinableString(
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                         DELETE_PROPAGATION_TYPE_NONE)
+                                     .SetCardinality(CARDINALITY_REPEATED)))
+            .AddType(
+                SchemaTypeConfigBuilder()
+                    .SetType("Label")
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("target")
+                                     .SetDataTypeJoinableString(
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                         DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                     .SetCardinality(CARDINALITY_REPEATED))
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("softTarget")
+                                     .SetDataTypeJoinableString(
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                         DELETE_PROPAGATION_TYPE_NONE)
+                                     .SetCardinality(CARDINALITY_REPEATED)))
+
+            .Build();
+    ASSERT_THAT(schema_store_->SetSchema(
+                    schema, /*ignore_errors_and_delete_documents=*/false),
+                IsOk());
+
+    ASSERT_THAT(filesystem_.CreateDirectoryRecursively(doc_store_dir_.c_str()),
+                IsTrue());
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::CreateResult create_result,
+        DocumentStore::Create(
+            &filesystem_, doc_store_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
+    doc_store_ = std::move(create_result.document_store);
+  }
+
+  void TearDown() override {
+    doc_store_.reset();
+    schema_store_.reset();
+    lang_segmenter_.reset();
+
+    filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
+  }
+
+  std::unique_ptr<FeatureFlags> feature_flags_;
+  Filesystem filesystem_;
+  FakeClock fake_clock_;
+  std::string test_dir_;
+  std::string schema_store_dir_;
+  std::string doc_store_dir_;
+
+  std::unique_ptr<LanguageSegmenter> lang_segmenter_;
+  std::unique_ptr<SchemaStore> schema_store_;
+  std::unique_ptr<DocumentStore> doc_store_;
+};
+
+TEST_F(DocumentDependencyProcessorTest,
+       Create_alreadyExpiredDocumentShouldFail) {
+  fake_clock_.SetSystemTimeMilliseconds(500);
+
+  DocumentProto email1 = DocumentBuilder()
+                             .SetCreationTimestampMs(100)
+                             .SetTtlMs(400)
+                             .SetKey("namespace", "email")
+                             .SetSchema("Email")
+                             .Build();
+  DocumentProto email2 = DocumentBuilder()
+                             .SetCreationTimestampMs(100)
+                             .SetTtlMs(300)
+                             .SetKey("namespace", "email")
+                             .SetSchema("Email")
+                             .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_email1,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), email1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_email2,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), email2));
+
+  std::vector<TokenizedDocument> batch_documents_to_add1;
+  batch_documents_to_add1.push_back(std::move(tokenized_doc_email1));
+
+  EXPECT_THAT(DocumentDependencyProcessor::Create(
+                  doc_store_.get(), batch_documents_to_add1,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+                       HasSubstr("The new document is expired.")));
+
+  std::vector<TokenizedDocument> batch_documents_to_add2;
+  batch_documents_to_add2.push_back(std::move(tokenized_doc_email2));
+
+  EXPECT_THAT(DocumentDependencyProcessor::Create(
+                  doc_store_.get(), batch_documents_to_add2,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+                       HasSubstr("The new document is expired.")));
+}
+
+TEST_F(DocumentDependencyProcessorTest, Evaluate) {
+  // This is a general test case to evaluate a batch of documents with all the
+  // possible scenarios:
+  // - Replace existing documents (i.e. replace documents that were previously
+  //   added into the document store).
+  // - Add new documents.
+  //
+  // And both of them have dependencies on existing, replaced and new documents.
+
+  // Create person1, person2, email1, label1, label2, label3 with the following
+  // relation:
+  //
+  // person1        person2 --> email1        label1 --> label2 --> label3
+  DocumentProto person1 = DocumentBuilder()
+                              .SetKey("namespace", "person1")
+                              .SetSchema("Person")
+                              .AddStringProperty("Name", "Alice")
+                              .Build();
+  DocumentProto person2 = DocumentBuilder()
+                              .SetKey("namespace", "person2")
+                              .SetSchema("Person")
+                              .AddStringProperty("Name", "Bob")
+                              .Build();
+  DocumentProto email1 = DocumentBuilder()
+                             .SetKey("namespace", "email1")
+                             .SetSchema("Email")
+                             .AddStringProperty("receiver", "namespace#person2")
+                             .Build();
+  DocumentProto label1 = DocumentBuilder()
+                             .SetKey("namespace", "label1")
+                             .SetSchema("Label")
+                             .Build();
+  DocumentProto label2 = DocumentBuilder()
+                             .SetKey("namespace", "label2")
+                             .SetSchema("Label")
+                             .AddStringProperty("target", "namespace#label1")
+                             .Build();
+  DocumentProto label3 = DocumentBuilder()
+                             .SetKey("namespace", "label3")
+                             .SetSchema("Label")
+                             .AddStringProperty("target", "namespace#label2")
+                             .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_person1,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          person1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_person2,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          person2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_email1,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), email1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_label1,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), label1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_label2,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), label2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_label3,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), label3));
+
+  std::vector<TokenizedDocument> batch_documents_to_add1;
+  batch_documents_to_add1.push_back(std::move(tokenized_doc_person1));
+  batch_documents_to_add1.push_back(std::move(tokenized_doc_person2));
+  batch_documents_to_add1.push_back(std::move(tokenized_doc_email1));
+  batch_documents_to_add1.push_back(std::move(tokenized_doc_label1));
+  batch_documents_to_add1.push_back(std::move(tokenized_doc_label2));
+  batch_documents_to_add1.push_back(std::move(tokenized_doc_label3));
+
+  // Evaluate all of them together should succeed.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentDependencyProcessor processor1,
+      DocumentDependencyProcessor::Create(
+          doc_store_.get(), batch_documents_to_add1,
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(processor1.Evaluate(), IsOk());
+
+  // Put them into the document store.
+  ICING_ASSERT_OK(doc_store_->Put(
+      batch_documents_to_add1[0].document_wrapper()));  // person1
+  ICING_ASSERT_OK(doc_store_->Put(
+      batch_documents_to_add1[1].document_wrapper()));  // person2
+  ICING_ASSERT_OK(doc_store_->Put(
+      batch_documents_to_add1[2].document_wrapper()));  // email1
+  ICING_ASSERT_OK(doc_store_->Put(
+      batch_documents_to_add1[3].document_wrapper()));  // label1
+  ICING_ASSERT_OK(doc_store_->Put(
+      batch_documents_to_add1[4].document_wrapper()));  // label2
+  ICING_ASSERT_OK(doc_store_->Put(
+      batch_documents_to_add1[5].document_wrapper()));  // label3
+
+  // Replace existing person2, email1, label1 and add new label4, label5 to make
+  // the following relation:
+  // (person1, label2, label3 are not changed).
+  //
+  // person1 -------+              +--------> label4 <--------+
+  // (unchanged)    |              |          (NEW)           |
+  //                v              |             |            |
+  //              email1 -----> label5           |         label2 -----> label3
+  //              (REPLACED)     (NEW)           |         (unchanged)
+  //                ^              ^             |            ^
+  //                |              |             v            |
+  // person2 -------+              +--------- label1 ---------+
+  // (REPLACED)                               (REPLACED)
+  DocumentProto person2_to_replace = DocumentBuilder()
+                                         .SetKey("namespace", "person2")
+                                         .SetSchema("Person")
+                                         .AddStringProperty("Name", "Robert")
+                                         .Build();
+  DocumentProto email1_to_replace =
+      DocumentBuilder()
+          .SetKey("namespace", "email1")
+          .SetSchema("Email")
+          .AddStringProperty("receiver", "namespace#person1",
+                             "namespace#person2")
+          .Build();
+  DocumentProto label1_to_replace =
+      DocumentBuilder()
+          .SetKey("namespace", "label1")
+          .SetSchema("Label")
+          .AddStringProperty("target", "namespace#label4")
+          .Build();
+  DocumentProto label4 =
+      DocumentBuilder()
+          .SetKey("namespace", "label4")
+          .SetSchema("Label")
+          .AddStringProperty("target", "namespace#label2", "namespace#label5")
+          .Build();
+  DocumentProto label5 =
+      DocumentBuilder()
+          .SetKey("namespace", "label5")
+          .SetSchema("Label")
+          .AddStringProperty("target", "namespace#label1", "namespace#email1")
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_person2_to_replace,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          person2_to_replace));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_email1_to_replace,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          email1_to_replace));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_label1_to_replace,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          label1_to_replace));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_label4,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), label4));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_label5,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), label5));
+
+  std::vector<TokenizedDocument> batch_documents_to_add2;
+  batch_documents_to_add2.push_back(
+      std::move(tokenized_doc_person2_to_replace));
+  batch_documents_to_add2.push_back(std::move(tokenized_doc_email1_to_replace));
+  batch_documents_to_add2.push_back(std::move(tokenized_doc_label1_to_replace));
+  batch_documents_to_add2.push_back(std::move(tokenized_doc_label4));
+  batch_documents_to_add2.push_back(std::move(tokenized_doc_label5));
+
+  // Evaluate all of them together should succeed.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentDependencyProcessor processor2,
+      DocumentDependencyProcessor::Create(
+          doc_store_.get(), batch_documents_to_add2,
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(processor2.Evaluate(), IsOk());
+}
+
+TEST_F(DocumentDependencyProcessorTest,
+       Evaluate_singleDocumentWithoutDependency) {
+  // Set the current time to 0.
+  fake_clock_.SetSystemTimeMilliseconds(0);
+
+  // Create a person document with expiration timestamp 2000.
+  DocumentProto person = DocumentBuilder()
+                             .SetCreationTimestampMs(0)
+                             .SetTtlMs(2000)
+                             .SetKey("namespace", "person")
+                             .SetSchema("Person")
+                             .AddStringProperty("Name", "Alice")
+                             .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_person,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), person));
+
+  std::vector<TokenizedDocument> batch_documents_to_add1;
+  batch_documents_to_add1.push_back(std::move(tokenized_doc_person));
+
+  // Evaluate person should succeed.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentDependencyProcessor processor,
+      DocumentDependencyProcessor::Create(
+          doc_store_.get(), batch_documents_to_add1,
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(processor.Evaluate(), IsOk());
+}
+
+TEST_F(DocumentDependencyProcessorTest,
+       Evaluate_nonExistentReferencedDocumentShouldFail) {
+  DocumentProto email = DocumentBuilder()
+                            .SetKey("namespace", "email")
+                            .SetSchema("Email")
+                            .AddStringProperty("receiver", "namespace#person")
+                            .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_email,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), email));
+
+  std::vector<TokenizedDocument> batch_documents_to_add;
+  batch_documents_to_add.push_back(std::move(tokenized_doc_email));
+
+  // Evaluate should fail since email's referenced document ("namespace#person")
+  // with delete propagation enabled doesn't exist.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentDependencyProcessor processor,
+      DocumentDependencyProcessor::Create(
+          doc_store_.get(), batch_documents_to_add,
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(processor.Evaluate(),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+                       HasSubstr("A dependency document is not alive")));
+}
+
+TEST_F(DocumentDependencyProcessorTest,
+       Evaluate_invalidReferencedQualifiedIdShouldFail) {
+  DocumentProto email =
+      DocumentBuilder()
+          .SetKey("namespace", "email")
+          .SetSchema("Email")
+          .AddStringProperty("receiver", "invalid_qualified_id")
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_email,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), email));
+
+  std::vector<TokenizedDocument> batch_documents_to_add;
+  batch_documents_to_add.push_back(std::move(tokenized_doc_email));
+
+  // Evaluate should fail since email contains an invalid qualified id in a
+  // joinable property with delete propagation enabled.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentDependencyProcessor processor,
+      DocumentDependencyProcessor::Create(
+          doc_store_.get(), batch_documents_to_add,
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(processor.Evaluate(),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+                       HasSubstr("Invalid qualified id string")));
+}
+
+TEST_F(DocumentDependencyProcessorTest,
+       Evaluate_emptyQualifiedIdStringShouldSucceed) {
+  DocumentProto email = DocumentBuilder()
+                            .SetKey("namespace", "email")
+                            .SetSchema("Email")
+                            .AddStringProperty("receiver", "")
+                            .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_email,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), email));
+
+  std::vector<TokenizedDocument> batch_documents_to_add;
+  batch_documents_to_add.push_back(std::move(tokenized_doc_email));
+
+  // Evaluate should succeed since empty qualified id string is allowed.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentDependencyProcessor processor,
+      DocumentDependencyProcessor::Create(
+          doc_store_.get(), batch_documents_to_add,
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(processor.Evaluate(), IsOk());
+}
+
+TEST_F(DocumentDependencyProcessorTest,
+       Evaluate_allReferencedDocumentsInBatchShouldSucceed) {
+  // Create person1, person2, email with the following relation:
+  //
+  // person1 -------+
+  //                |
+  //                v
+  //              email
+  //                ^
+  //                |
+  // person2 -------+
+  //
+  // (email has 2 parent documents person1 and person2)
+  DocumentProto person1 = DocumentBuilder()
+                              .SetKey("namespace", "person1")
+                              .SetSchema("Person")
+                              .AddStringProperty("Name", "Alice")
+                              .Build();
+  DocumentProto person2 = DocumentBuilder()
+                              .SetKey("namespace", "person2")
+                              .SetSchema("Person")
+                              .AddStringProperty("Name", "Bob")
+                              .Build();
+  DocumentProto email = DocumentBuilder()
+                            .SetKey("namespace", "email")
+                            .SetSchema("Email")
+                            .AddStringProperty("receiver", "namespace#person1",
+                                               "namespace#person2")
+                            .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_person1,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          person1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_person2,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          person2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_email,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), email));
+
+  std::vector<TokenizedDocument> batch_documents_to_add;
+  batch_documents_to_add.push_back(std::move(tokenized_doc_person1));
+  batch_documents_to_add.push_back(std::move(tokenized_doc_person2));
+  batch_documents_to_add.push_back(std::move(tokenized_doc_email));
+
+  // Evaluate all of them together should succeed.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentDependencyProcessor processor,
+      DocumentDependencyProcessor::Create(
+          doc_store_.get(), batch_documents_to_add,
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(processor.Evaluate(), IsOk());
+}
+
+TEST_F(DocumentDependencyProcessorTest,
+       Evaluate_selfReferenceInBatchShouldSucceed) {
+  // Create label document having a self reference on "target" property with
+  // delete propagation enabled.
+  DocumentProto label = DocumentBuilder()
+                            .SetKey("namespace", "label")
+                            .SetSchema("Label")
+                            .AddStringProperty("target", "namespace#label")
+                            .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_label,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), label));
+
+  std::vector<TokenizedDocument> batch_documents_to_add;
+  batch_documents_to_add.push_back(std::move(tokenized_doc_label));
+
+  // Evaluate should succeed since the referenced document is present in the
+  // same batch of documents to add.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentDependencyProcessor processor,
+      DocumentDependencyProcessor::Create(
+          doc_store_.get(), batch_documents_to_add,
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(processor.Evaluate(), IsOk());
+}
+
+TEST_F(DocumentDependencyProcessorTest,
+       Evaluate_cycleReferenceInBatchShouldSucceed) {
+  // Create label1, label2, label3 with the following relation:
+  //
+  // label1 -> label2 -> label3
+  //   ^                   |
+  //   |                   |
+  //   +-------------------+
+  DocumentProto label1 = DocumentBuilder()
+                             .SetKey("namespace", "label1")
+                             .SetSchema("Label")
+                             .AddStringProperty("target", "namespace#label3")
+                             .Build();
+  DocumentProto label2 = DocumentBuilder()
+                             .SetKey("namespace", "label2")
+                             .SetSchema("Label")
+                             .AddStringProperty("target", "namespace#label1")
+                             .Build();
+  DocumentProto label3 = DocumentBuilder()
+                             .SetKey("namespace", "label3")
+                             .SetSchema("Label")
+                             .AddStringProperty("target", "namespace#label2")
+                             .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_label1,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), label1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_label2,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), label2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_label3,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), label3));
+
+  std::vector<TokenizedDocument> batch_documents_to_add;
+  batch_documents_to_add.push_back(std::move(tokenized_doc_label1));
+  batch_documents_to_add.push_back(std::move(tokenized_doc_label2));
+  batch_documents_to_add.push_back(std::move(tokenized_doc_label3));
+
+  // Evaluate should succeed since the all referenced documents are present in
+  // the same batch of documents to add.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentDependencyProcessor processor,
+      DocumentDependencyProcessor::Create(
+          doc_store_.get(), batch_documents_to_add,
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(processor.Evaluate(), IsOk());
+}
+
+TEST_F(DocumentDependencyProcessorTest,
+       Evaluate_allReferencedDocumentsInDocumentStoreShouldSucceed) {
+  // Create person1, person2, email with the following relation:
+  //
+  // person1 -------+
+  //                |
+  //                v
+  //              email
+  //                ^
+  //                |
+  // person2 -------+
+  //
+  // (email has 2 parent documents person1 and person2)
+  DocumentProto person1 = DocumentBuilder()
+                              .SetKey("namespace", "person1")
+                              .SetSchema("Person")
+                              .AddStringProperty("Name", "Alice")
+                              .Build();
+  DocumentProto person2 = DocumentBuilder()
+                              .SetKey("namespace", "person2")
+                              .SetSchema("Person")
+                              .AddStringProperty("Name", "Bob")
+                              .Build();
+  DocumentProto email = DocumentBuilder()
+                            .SetKey("namespace", "email")
+                            .SetSchema("Email")
+                            .AddStringProperty("receiver", "namespace#person1",
+                                               "namespace#person2")
+                            .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_email,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), email));
+
+  // Put person1, person2 into the document store.
+  ICING_ASSERT_OK(
+      doc_store_->Put(document_util::CreateDocumentWrapper(person1)));
+  ICING_ASSERT_OK(
+      doc_store_->Put(document_util::CreateDocumentWrapper(person2)));
+
+  std::vector<TokenizedDocument> batch_documents_to_add;
+  batch_documents_to_add.push_back(std::move(tokenized_doc_email));
+
+  // Evaluate email should succeed.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentDependencyProcessor processor,
+      DocumentDependencyProcessor::Create(
+          doc_store_.get(), batch_documents_to_add,
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(processor.Evaluate(), IsOk());
+}
+
+TEST_F(DocumentDependencyProcessorTest,
+       Evaluate_expiredReferencedDocumentInDocumentStoreShouldFail) {
+  fake_clock_.SetSystemTimeMilliseconds(500);
+
+  // Create person1, person2, email with the following relation:
+  //
+  // person1 -------+
+  //                |
+  //                v
+  //              email
+  //                ^
+  //                |
+  // person2 -------+
+  //
+  // (email has 2 parent documents person1 and person2)
+  DocumentProto person1 = DocumentBuilder()
+                              .SetKey("namespace", "person1")
+                              .SetTtlMs(1000)
+                              .SetCreationTimestampMs(500)
+                              .SetSchema("Person")
+                              .AddStringProperty("Name", "Alice")
+                              .Build();
+  DocumentProto person2 = DocumentBuilder()
+                              .SetKey("namespace", "person2")
+                              .SetTtlMs(100)
+                              .SetCreationTimestampMs(500)
+                              .SetSchema("Person")
+                              .AddStringProperty("Name", "Bob")
+                              .Build();
+  DocumentProto email = DocumentBuilder()
+                            .SetKey("namespace", "email")
+                            .SetSchema("Email")
+                            .AddStringProperty("receiver", "namespace#person1",
+                                               "namespace#person2")
+                            .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_email,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), email));
+
+  // Put person1, person2 into the document store.
+  ICING_ASSERT_OK(
+      doc_store_->Put(document_util::CreateDocumentWrapper(person1)));
+  ICING_ASSERT_OK(
+      doc_store_->Put(document_util::CreateDocumentWrapper(person2)));
+
+  // Adjust the current time to make person2 expired, but person1 is still
+  // alive.
+  fake_clock_.SetSystemTimeMilliseconds(1000);
+
+  std::vector<TokenizedDocument> batch_documents_to_add;
+  batch_documents_to_add.push_back(std::move(tokenized_doc_email));
+
+  // Evaluate email should fail since one of email's referenced documents
+  // (person2) with delete propagation enabled is expired.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentDependencyProcessor processor,
+      DocumentDependencyProcessor::Create(
+          doc_store_.get(), batch_documents_to_add,
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(processor.Evaluate(),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+                       HasSubstr("A dependency document is not alive")));
+}
+
+TEST_F(DocumentDependencyProcessorTest,
+       Evaluate_deletedReferencedDocumentInDocumentStoreShouldFail) {
+  // Create person1, person2, email with the following relation:
+  //
+  // person1 -------+
+  //                |
+  //                v
+  //              email
+  //                ^
+  //                |
+  // person2 -------+
+  //
+  // (email has 2 parent documents person1 and person2)
+  DocumentProto person1 = DocumentBuilder()
+                              .SetKey("namespace", "person1")
+                              .SetSchema("Person")
+                              .AddStringProperty("Name", "Alice")
+                              .Build();
+  DocumentProto person2 = DocumentBuilder()
+                              .SetKey("namespace", "person2")
+                              .SetSchema("Person")
+                              .AddStringProperty("Name", "Bob")
+                              .Build();
+  DocumentProto email = DocumentBuilder()
+                            .SetKey("namespace", "email")
+                            .SetSchema("Email")
+                            .AddStringProperty("receiver", "namespace#person1",
+                                               "namespace#person2")
+                            .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_email,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), email));
+
+  // Put person1, person2 into the document store.
+  ICING_ASSERT_OK(
+      doc_store_->Put(document_util::CreateDocumentWrapper(person1)));
+  ICING_ASSERT_OK(
+      doc_store_->Put(document_util::CreateDocumentWrapper(person2)));
+
+  // Delete person2.
+  ICING_ASSERT_OK(doc_store_->Delete(
+      "namespace", "person2",
+      /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+
+  // Evaluate email should fail since one of email's referenced documents
+  // (person2) with delete propagation enabled is deleted.
+  std::vector<TokenizedDocument> batch_documents_to_add;
+  batch_documents_to_add.push_back(std::move(tokenized_doc_email));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentDependencyProcessor processor,
+      DocumentDependencyProcessor::Create(
+          doc_store_.get(), batch_documents_to_add,
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(processor.Evaluate(),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+                       HasSubstr("A dependency document is not alive")));
+}
+
+TEST_F(DocumentDependencyProcessorTest,
+       Evaluate_withoutDeletePropagationShouldAlwaysSucceed) {
+  fake_clock_.SetSystemTimeMilliseconds(500);
+
+  // Create email document having an invalid qualified id string on "sender"
+  // property with delete propagation disabled. Evaluate should succeed since
+  // Icing will ignore invalid qualified id with delete propagation disabled.
+  DocumentProto email1 =
+      DocumentBuilder()
+          .SetKey("namespace", "email")
+          .SetSchema("Email")
+          .AddStringProperty("sender", "invalid_qualified_id")
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_email1,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), email1));
+  std::vector<TokenizedDocument> batch_documents_to_add1;
+  batch_documents_to_add1.push_back(std::move(tokenized_doc_email1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentDependencyProcessor processor1,
+      DocumentDependencyProcessor::Create(
+          doc_store_.get(), batch_documents_to_add1,
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(processor1.Evaluate(), IsOk());
+
+  // Create email document having a valid qualified id string on "sender"
+  // property with delete propagation disabled, but the referenced document
+  // doesn't exist. Evaluate should succeed since Icing will ignore non-existent
+  // referenced document with delete propagation disabled.
+  DocumentProto email2 = DocumentBuilder()
+                             .SetKey("namespace", "email")
+                             .SetSchema("Email")
+                             .AddStringProperty("sender", "namespace#person")
+                             .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_email2_1,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), email2));
+  std::vector<TokenizedDocument> batch_documents_to_add2_1;
+  batch_documents_to_add2_1.push_back(std::move(tokenized_doc_email2_1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentDependencyProcessor processor2_1,
+      DocumentDependencyProcessor::Create(
+          doc_store_.get(), batch_documents_to_add2_1,
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(processor2_1.Evaluate(), IsOk());
+
+  // Add person document into the document store to make email2's referenced
+  // document exist.
+  DocumentProto person = DocumentBuilder()
+                             .SetTtlMs(100)
+                             .SetCreationTimestampMs(500)
+                             .SetKey("namespace", "person")
+                             .SetSchema("Person")
+                             .AddStringProperty("Name", "Test Name")
+                             .Build();
+  ICING_ASSERT_OK(
+      doc_store_->Put(document_util::CreateDocumentWrapper(person)));
+
+  // Evaluate email2 again. Should succeed.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_email2_2,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), email2));
+  std::vector<TokenizedDocument> batch_documents_to_add2_2;
+  batch_documents_to_add2_2.push_back(std::move(tokenized_doc_email2_2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentDependencyProcessor processor2_2,
+      DocumentDependencyProcessor::Create(
+          doc_store_.get(), batch_documents_to_add2_2,
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(processor2_2.Evaluate(), IsOk());
+
+  // Evaluate email2 again with a different current time which makes person
+  // document expired. Since delete propagation is disabled, Evaluate should
+  // still succeed.
+  fake_clock_.SetSystemTimeMilliseconds(1000);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_doc_email2_3,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(), email2));
+  std::vector<TokenizedDocument> batch_documents_to_add2_3;
+  batch_documents_to_add2_3.push_back(std::move(tokenized_doc_email2_3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentDependencyProcessor processor2_3,
+      DocumentDependencyProcessor::Create(
+          doc_store_.get(), batch_documents_to_add2_3,
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(processor2_3.Evaluate(), IsOk());
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/join/document-dependent-graph.cc b/icing/join/document-dependent-graph.cc
new file mode 100644
index 0000000..6665b83
--- /dev/null
+++ b/icing/join/document-dependent-graph.cc
@@ -0,0 +1,127 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/join/document-dependent-graph.h"
+
+#include <memory>
+#include <optional>
+#include <utility>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/absl_ports/canonical_errors.h"
+#include "icing/graph/graph-interface.h"
+#include "icing/join/qualified-id-join-index.h"
+#include "icing/schema/joinable-property.h"
+#include "icing/schema/schema-store.h"
+#include "icing/store/document-filter-data.h"
+#include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
+#include "icing/util/status-macros.h"
+
+namespace icing {
+namespace lib {
+
+/* static */ libtextclassifier3::StatusOr<
+    std::unique_ptr<DocumentDependentGraph>>
+DocumentDependentGraph::Create(const SchemaStore* schema_store,
+                               const DocumentStore* doc_store,
+                               const QualifiedIdJoinIndex* join_index) {
+  ICING_RETURN_ERROR_IF_NULL(schema_store);
+  ICING_RETURN_ERROR_IF_NULL(doc_store);
+  ICING_RETURN_ERROR_IF_NULL(join_index);
+
+  if (join_index->version() != QualifiedIdJoinIndex::Version::kV3) {
+    return absl_ports::InvalidArgumentError(
+        "DocumentDependentGraph only supports QualifiedIdJoinIndex version "
+        "V3.");
+  }
+
+  return std::unique_ptr<DocumentDependentGraph>(
+      new DocumentDependentGraph(schema_store, doc_store, join_index));
+}
+
+int DocumentDependentGraph::GetNumNodes() const {
+  DocumentId last_stored_doc_id = doc_store_.last_added_document_id();
+  if (last_stored_doc_id == kInvalidDocumentId) {
+    return 0;
+  }
+  // There are documents from 0 to last_stored_doc_id, so num nodes
+  // should be last_stored_doc_id + 1.
+  return last_stored_doc_id + 1;
+}
+
+libtextclassifier3::StatusOr<
+    std::unique_ptr<typename graph::GraphInterface<DocumentId>::EdgeIteratorIf>>
+DocumentDependentGraph::GetEdgesIterator(int node_id) const {
+  if (node_id < 0 || node_id >= GetNumNodes()) {
+    return absl_ports::InvalidArgumentError("Invalid node id.");
+  }
+
+  DocumentId doc_id = static_cast<DocumentId>(node_id);
+  if (!doc_store_.GetNonDeletedDocumentFilterData(doc_id).has_value()) {
+    // Return an iterator with no edge to advance to for a deleted document.
+    return std::make_unique<EdgeIterator>(
+        schema_store_, doc_store_,
+        QualifiedIdJoinIndex::DocumentJoinIdPairArrayView(/*data=*/nullptr,
+                                                          /*size=*/0));
+  }
+
+  ICING_ASSIGN_OR_RETURN(
+      QualifiedIdJoinIndex::DocumentJoinIdPairArrayView data_array_view,
+      join_index_.GetDocumentJoinIdPairArrayView(doc_id));
+  return std::make_unique<EdgeIterator>(schema_store_, doc_store_,
+                                        std::move(data_array_view));
+}
+
+libtextclassifier3::Status DocumentDependentGraph::EdgeIterator::Advance() {
+  while (++curr_idx_ < join_data_array_view_.size()) {
+    DocumentId next_doc_id = join_data_array_view_[curr_idx_].document_id();
+    JoinablePropertyId next_joinable_property_id =
+        join_data_array_view_[curr_idx_].joinable_property_id();
+
+    // Dedupe document id.
+    if (next_doc_id == curr_) {
+      continue;
+    }
+
+    // Lookup the schema type id of the next document.
+    std::optional<DocumentFilterData> next_doc_filter_data =
+        doc_store_.GetNonDeletedDocumentFilterData(next_doc_id);
+    if (!next_doc_filter_data.has_value() ||
+        next_doc_filter_data->schema_type_id() == kInvalidSchemaTypeId) {
+      continue;
+    }
+
+    // Get the joinable property metadata.
+    ICING_ASSIGN_OR_RETURN(
+        const JoinablePropertyMetadata* metadata,
+        schema_store_.GetJoinablePropertyMetadata(
+            next_doc_filter_data->schema_type_id(), next_joinable_property_id));
+
+    if (metadata->delete_propagation_type ==
+        JoinableConfig::DeletePropagationType::PROPAGATE_FROM) {
+      // Found a joinable property hit with delete propagation PROPAGATE_FROM.
+      // It means the next document is a dependent of the current document, so
+      // stop here, record the doc id and return success.
+      curr_ = next_doc_id;
+      return libtextclassifier3::Status::OK;
+    }
+  }
+
+  return absl_ports::ResourceExhaustedError("No more edges to advance to.");
+}
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/join/document-dependent-graph.h b/icing/join/document-dependent-graph.h
new file mode 100644
index 0000000..f516400
--- /dev/null
+++ b/icing/join/document-dependent-graph.h
@@ -0,0 +1,102 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef ICING_JOIN_DOCUMENT_DEPENDENT_GRAPH_H_
+#define ICING_JOIN_DOCUMENT_DEPENDENT_GRAPH_H_
+
+#include <memory>
+#include <utility>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/graph/graph-interface.h"
+#include "icing/join/qualified-id-join-index.h"
+#include "icing/schema/schema-store.h"
+#include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
+
+namespace icing {
+namespace lib {
+
+// Document dependent graph using qualified id join index v3 as the data source.
+// This class is just an interface for the dependent graph and does not own any
+// data. Instead, it reads data from the given data sources and returns
+// dependent relations.
+//
+// Dependent graph:
+// - The nodes are the documents, and the node id type is DocumentId.
+// - The edges are directed and represent the dependencies between documents.
+//   For example, an edge A -> B means:
+//   - A is a dependency of B.
+//   - B is a dependent of A.
+//   - If A is deleted or expired, then B should be deleted or expired as well.
+class DocumentDependentGraph : public graph::GraphInterface<DocumentId> {
+ public:
+  // Creates a document dependent graph object.
+  //
+  // Returns:
+  //   - Non-null unique pointer of DocumentDependentGraph on success.
+  //   - FAILED_PRECONDITION_ERROR if any data source is null.
+  //   - INVALID_ARGUMENT_ERROR if join_index is not version V3.
+  //   - Any errors from the underlying data source.
+  static libtextclassifier3::StatusOr<std::unique_ptr<DocumentDependentGraph>>
+  Create(const SchemaStore* schema_store, const DocumentStore* doc_store,
+         const QualifiedIdJoinIndex* join_index);
+
+  int GetNumNodes() const override;
+
+  libtextclassifier3::StatusOr<std::unique_ptr<EdgeIteratorIf>>
+  GetEdgesIterator(int node_id) const override;
+
+ private:
+  class EdgeIterator : public EdgeIteratorIf {
+   public:
+    explicit EdgeIterator(
+        const SchemaStore& schema_store, const DocumentStore& doc_store,
+        QualifiedIdJoinIndex::DocumentJoinIdPairArrayView join_data_array_view)
+        : schema_store_(schema_store),
+          doc_store_(doc_store),
+          join_data_array_view_(std::move(join_data_array_view)),
+          curr_idx_(-1),
+          curr_(kInvalidDocumentId) {}
+
+    libtextclassifier3::Status Advance() override;
+
+    const DocumentId& Get() const override { return curr_; }
+
+   private:
+    const SchemaStore& schema_store_;  // Does not own.
+    const DocumentStore& doc_store_;   // Does not own.
+
+    QualifiedIdJoinIndex::DocumentJoinIdPairArrayView join_data_array_view_;
+    int curr_idx_;
+    DocumentId curr_;
+  };
+
+  explicit DocumentDependentGraph(const SchemaStore* schema_store,
+                                  const DocumentStore* doc_store,
+                                  const QualifiedIdJoinIndex* join_index)
+      : schema_store_(*schema_store),
+        doc_store_(*doc_store),
+        join_index_(*join_index) {}
+
+  const SchemaStore& schema_store_;         // Does not own.
+  const DocumentStore& doc_store_;          // Does not own.
+  const QualifiedIdJoinIndex& join_index_;  // Does not own.
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_JOIN_DOCUMENT_DEPENDENT_GRAPH_H_
diff --git a/icing/join/document-dependent-graph_test.cc b/icing/join/document-dependent-graph_test.cc
new file mode 100644
index 0000000..c08e67c
--- /dev/null
+++ b/icing/join/document-dependent-graph_test.cc
@@ -0,0 +1,858 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/join/document-dependent-graph.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "icing/document-builder.h"
+#include "icing/feature-flags.h"
+#include "icing/file/filesystem.h"
+#include "icing/file/portable-file-backed-proto-log.h"
+#include "icing/graph/graph-interface.h"
+#include "icing/join/document-join-id-pair.h"
+#include "icing/join/qualified-id-join-index-impl-v2.h"
+#include "icing/join/qualified-id-join-index-impl-v3.h"
+#include "icing/join/qualified-id-join-indexing-handler.h"
+#include "icing/portable/gzip_stream.h"
+#include "icing/portable/platform.h"
+#include "icing/proto/document.pb.h"
+#include "icing/proto/document_wrapper.pb.h"
+#include "icing/proto/schema.pb.h"
+#include "icing/proto/scoring.pb.h"
+#include "icing/proto/search.pb.h"
+#include "icing/schema-builder.h"
+#include "icing/schema/schema-store.h"
+#include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
+#include "icing/testing/common-matchers.h"
+#include "icing/testing/fake-clock.h"
+#include "icing/testing/test-data.h"
+#include "icing/testing/test-feature-flags.h"
+#include "icing/testing/tmp-directory.h"
+#include "icing/tokenization/language-segmenter-factory.h"
+#include "icing/tokenization/language-segmenter.h"
+#include "icing/util/icu-data-file-helper.h"
+#include "icing/util/status-macros.h"
+#include "icing/util/tokenized-document.h"
+#include "unicode/uloc.h"
+
+namespace icing {
+namespace lib {
+
+namespace {
+
+using ::testing::ElementsAre;
+using ::testing::Eq;
+using ::testing::HasSubstr;
+using ::testing::IsEmpty;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
+using ::testing::NotNull;
+
+class DocumentDependentGraphTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+    ASSERT_THAT(feature_flags_->enable_repeated_field_joins(), IsTrue());
+
+    test_dir_ = GetTestTempDir() + "/document_dependent_graph_test";
+    ASSERT_THAT(filesystem_.CreateDirectoryRecursively(test_dir_.c_str()),
+                IsTrue());
+
+    schema_store_dir_ = test_dir_ + "/schema_store";
+    doc_store_dir_ = test_dir_ + "/doc_store";
+    join_index_dir_ = test_dir_ + "/join_index";
+
+    if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
+      ICING_ASSERT_OK(
+          // File generated via icu_data_file rule in //icing/BUILD.
+          icu_data_file_helper::SetUpIcuDataFile(
+              GetTestFilePath("icing/icu.dat")));
+    }
+
+    language_segmenter_factory::SegmenterOptions options(ULOC_US);
+    ICING_ASSERT_OK_AND_ASSIGN(
+        lang_segmenter_,
+        language_segmenter_factory::Create(std::move(options)));
+
+    ASSERT_THAT(
+        filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str()),
+        IsTrue());
+    ICING_ASSERT_OK_AND_ASSIGN(
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
+
+    SchemaProto schema =
+        SchemaBuilder()
+            .AddType(
+                SchemaTypeConfigBuilder()
+                    .SetType("Label")
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("target")
+                                     .SetDataTypeJoinableString(
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                         DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                     .SetCardinality(CARDINALITY_REPEATED))
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("target2")
+                                     .SetDataTypeJoinableString(
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                         DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                                     .SetCardinality(CARDINALITY_REPEATED))
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("softTarget")
+                                     .SetDataTypeJoinableString(
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                         DELETE_PROPAGATION_TYPE_NONE)
+                                     .SetCardinality(CARDINALITY_REPEATED)))
+
+            .Build();
+    ICING_ASSERT_OK(schema_store_->SetSchema(
+        schema, /*ignore_errors_and_delete_documents=*/false));
+
+    ASSERT_THAT(filesystem_.CreateDirectoryRecursively(doc_store_dir_.c_str()),
+                IsTrue());
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::CreateResult create_result,
+        DocumentStore::Create(
+            &filesystem_, doc_store_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
+    doc_store_ = std::move(create_result.document_store);
+
+    ICING_ASSERT_OK_AND_ASSIGN(
+        join_index_, QualifiedIdJoinIndexImplV3::Create(
+                         filesystem_, join_index_dir_, *feature_flags_));
+    ICING_ASSERT_OK_AND_ASSIGN(join_indexing_handler_,
+                               QualifiedIdJoinIndexingHandler::Create(
+                                   &fake_clock_, doc_store_.get(),
+                                   join_index_.get(), feature_flags_.get()));
+  }
+
+  void TearDown() override {
+    join_indexing_handler_.reset();
+
+    join_index_.reset();
+    doc_store_.reset();
+    schema_store_.reset();
+    lang_segmenter_.reset();
+
+    filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
+  }
+
+  // Helper function to batch add documents.
+  libtextclassifier3::Status AddDocuments(
+      std::vector<DocumentProto> documents) {
+    // Tokenize all documents.
+    std::vector<TokenizedDocument> tokenized_documents;
+    tokenized_documents.reserve(documents.size());
+    for (DocumentProto& document : documents) {
+      ICING_ASSIGN_OR_RETURN(
+          TokenizedDocument tokenized_document,
+          TokenizedDocument::Create(
+              schema_store_.get(), lang_segmenter_.get(),
+              /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+              std::move(document)));
+      tokenized_documents.push_back(std::move(tokenized_document));
+    }
+
+    // Put all documents into the document store and get document ids.
+    std::vector<DocumentStore::PutResult> put_results;
+    put_results.reserve(documents.size());
+    for (const TokenizedDocument& tokenized_document : tokenized_documents) {
+      ICING_ASSIGN_OR_RETURN(
+          DocumentStore::PutResult put_result,
+          doc_store_->Put(tokenized_document.document_wrapper()));
+      put_results.push_back(std::move(put_result));
+    }
+
+    // Index all documents.
+    for (int i = 0; i < tokenized_documents.size(); ++i) {
+      ICING_RETURN_IF_ERROR(join_indexing_handler_->Handle(
+          tokenized_documents[i], put_results[i].new_document_id,
+          put_results[i].old_document_id, /*recovery_mode=*/false,
+          /*put_document_stats=*/nullptr));
+    }
+    return libtextclassifier3::Status::OK;
+  }
+
+  libtextclassifier3::StatusOr<std::vector<DocumentId>> GetEdgesOfNode(
+      const DocumentDependentGraph& graph, int node_id) {
+    ICING_ASSIGN_OR_RETURN(
+        std::unique_ptr<
+            typename graph::GraphInterface<DocumentId>::EdgeIteratorIf>
+            edge_itr,
+        graph.GetEdgesIterator(node_id));
+    std::vector<DocumentId> edges;
+    while (edge_itr->Advance().ok()) {
+      edges.push_back(edge_itr->Get());
+    }
+    return edges;
+  }
+
+  std::unique_ptr<FeatureFlags> feature_flags_;
+  Filesystem filesystem_;
+  FakeClock fake_clock_;
+  std::string test_dir_;
+  std::string schema_store_dir_;
+  std::string doc_store_dir_;
+  std::string join_index_dir_;
+
+  std::unique_ptr<LanguageSegmenter> lang_segmenter_;
+  std::unique_ptr<SchemaStore> schema_store_;
+  std::unique_ptr<DocumentStore> doc_store_;
+  std::unique_ptr<QualifiedIdJoinIndexImplV3> join_index_;
+
+  std::unique_ptr<QualifiedIdJoinIndexingHandler> join_indexing_handler_;
+};
+
+TEST_F(DocumentDependentGraphTest, CreationWithNullPointerShouldFail) {
+  EXPECT_THAT(
+      DocumentDependentGraph::Create(/*schema_store=*/nullptr, doc_store_.get(),
+                                     join_index_.get()),
+      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(
+      DocumentDependentGraph::Create(schema_store_.get(), /*doc_store=*/nullptr,
+                                     join_index_.get()),
+      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(
+      DocumentDependentGraph::Create(schema_store_.get(), doc_store_.get(),
+                                     /*join_index=*/nullptr),
+      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+}
+
+TEST_F(DocumentDependentGraphTest,
+       CreationWithWrongJoinIndexVersionShouldFail) {
+  std::string join_index_v2_dir = test_dir_ + "/join_index_v2";
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<QualifiedIdJoinIndexImplV2> join_index_v2,
+      QualifiedIdJoinIndexImplV2::Create(filesystem_, join_index_v2_dir,
+                                         /*pre_mapping_fbv=*/false));
+
+  EXPECT_THAT(DocumentDependentGraph::Create(
+                  schema_store_.get(), doc_store_.get(), join_index_v2.get()),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+                       HasSubstr("DocumentDependentGraph only supports "
+                                 "QualifiedIdJoinIndex version V3.")));
+}
+
+TEST_F(DocumentDependentGraphTest, GetNumNodes) {
+  // Put and index 4 documents.
+  DocumentProto doc0 =
+      DocumentBuilder().SetKey("namespace", "uri/0").SetSchema("Label").Build();
+  DocumentProto doc1 =
+      DocumentBuilder().SetKey("namespace", "uri/1").SetSchema("Label").Build();
+  DocumentProto doc2 =
+      DocumentBuilder().SetKey("namespace", "uri/2").SetSchema("Label").Build();
+  DocumentProto doc3 =
+      DocumentBuilder().SetKey("namespace", "uri/3").SetSchema("Label").Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1, doc2, doc3}));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocumentDependentGraph> graph,
+      DocumentDependentGraph::Create(schema_store_.get(), doc_store_.get(),
+                                     join_index_.get()));
+  EXPECT_THAT(graph->GetNumNodes(), Eq(4));
+}
+
+TEST_F(DocumentDependentGraphTest, GetNumNodes_emptyStorage) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocumentDependentGraph> graph,
+      DocumentDependentGraph::Create(schema_store_.get(), doc_store_.get(),
+                                     join_index_.get()));
+  EXPECT_THAT(graph->GetNumNodes(), Eq(0));
+}
+
+TEST_F(DocumentDependentGraphTest, GetNumNodes_withReplacedDocuments) {
+  // Put and index 1 document.
+  DocumentProto doc =
+      DocumentBuilder().SetKey("namespace", "uri").SetSchema("Label").Build();
+  ICING_ASSERT_OK(AddDocuments({doc}));
+
+  // Replace the document with new content.
+  DocumentProto doc_replaced = DocumentBuilder()
+                                   .SetKey("namespace", "uri")
+                                   .SetSchema("Label")
+                                   .AddStringProperty("target", "namespace#uri")
+                                   .Build();
+  ICING_ASSERT_OK(AddDocuments({doc_replaced}));
+
+  // Sanity check.
+  ASSERT_THAT(doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/0),
+              IsFalse());
+  ASSERT_THAT(doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/1),
+              IsTrue());
+
+  // Even though document 0 is replaced, num nodes should still be 2 since it is
+  // computed based on last stored doc id.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocumentDependentGraph> graph,
+      DocumentDependentGraph::Create(schema_store_.get(), doc_store_.get(),
+                                     join_index_.get()));
+  EXPECT_THAT(graph->GetNumNodes(), Eq(2));
+}
+
+TEST_F(DocumentDependentGraphTest, GetNumNodes_withDeletedDocuments) {
+  // Put and index 1 document.
+  DocumentProto doc =
+      DocumentBuilder().SetKey("namespace", "uri").SetSchema("Label").Build();
+  ICING_ASSERT_OK(AddDocuments({doc}));
+
+  // Delete the document.
+  ICING_ASSERT_OK(doc_store_->Delete(
+      /*document_id=*/0,
+      /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+
+  // Sanity check.
+  ASSERT_THAT(doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/0),
+              IsFalse());
+
+  // Even though document 0 is deleted, num nodes should still be 1 since it is
+  // computed based on last stored doc id.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocumentDependentGraph> graph,
+      DocumentDependentGraph::Create(schema_store_.get(), doc_store_.get(),
+                                     join_index_.get()));
+  EXPECT_THAT(graph->GetNumNodes(), Eq(1));
+}
+
+TEST_F(DocumentDependentGraphTest, GetNumNodes_withExpiredDocuments) {
+  fake_clock_.SetSystemTimeMilliseconds(0);
+
+  // Put and index 1 document which expires at 1000 ms.
+  DocumentProto doc = DocumentBuilder()
+                          .SetCreationTimestampMs(0)
+                          .SetTtlMs(1000)
+                          .SetKey("namespace", "uri")
+                          .SetSchema("Label")
+                          .Build();
+  ICING_ASSERT_OK(AddDocuments({doc}));
+
+  // Adjust the clock to expire the document.
+  fake_clock_.SetSystemTimeMilliseconds(2000);
+
+  // Sanity check.
+  ASSERT_THAT(doc_store_->GetAliveDocumentFilterData(
+                  /*document_id=*/0,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsFalse());
+  ASSERT_THAT(doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/0),
+              IsTrue());
+
+  // Even though document 0 is expired, num nodes should still be 1 since it is
+  // computed based on last stored doc id.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocumentDependentGraph> graph,
+      DocumentDependentGraph::Create(schema_store_.get(), doc_store_.get(),
+                                     join_index_.get()));
+  EXPECT_THAT(graph->GetNumNodes(), Eq(1));
+}
+
+TEST_F(DocumentDependentGraphTest, GetEdgesIterator) {
+  // Put and index 5 documents with the following relations:
+  //
+  // doc0 ---+
+  //         |
+  //         +---> Doc2 --> Doc3
+  //         |
+  // doc1 ---+
+  //   |
+  //   +---------> Doc4
+  DocumentProto doc0 =
+      DocumentBuilder().SetKey("namespace", "uri/0").SetSchema("Label").Build();
+  DocumentProto doc1 =
+      DocumentBuilder().SetKey("namespace", "uri/1").SetSchema("Label").Build();
+  DocumentProto doc2 =
+      DocumentBuilder()
+          .SetKey("namespace", "uri/2")
+          .SetSchema("Label")
+          .AddStringProperty("target", "namespace#uri/0", "namespace#uri/1")
+          .Build();
+  DocumentProto doc3 = DocumentBuilder()
+                           .SetKey("namespace", "uri/3")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/2")
+                           .Build();
+  DocumentProto doc4 = DocumentBuilder()
+                           .SetKey("namespace", "uri/4")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/1")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1, doc2, doc3, doc4}));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocumentDependentGraph> graph,
+      DocumentDependentGraph::Create(schema_store_.get(), doc_store_.get(),
+                                     join_index_.get()));
+  ASSERT_THAT(graph->GetNumNodes(), Eq(5));
+
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/0),
+              IsOkAndHolds(ElementsAre(2)));
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/1),
+              IsOkAndHolds(ElementsAre(2, 4)));
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/2),
+              IsOkAndHolds(ElementsAre(3)));
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/3), IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/4), IsOkAndHolds(IsEmpty()));
+}
+
+TEST_F(DocumentDependentGraphTest,
+       GetEdgesIterator_withoutDeletePropagationShouldNotBeIncludedIntoEdges) {
+  // Put and index 2 documents with the following relations:
+  //
+  // doc0 --(no delete propagation)--> doc1
+  DocumentProto doc0 =
+      DocumentBuilder().SetKey("namespace", "uri/0").SetSchema("Label").Build();
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .AddStringProperty("softTarget", "namespace#uri/0")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1}));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocumentDependentGraph> graph,
+      DocumentDependentGraph::Create(schema_store_.get(), doc_store_.get(),
+                                     join_index_.get()));
+  ASSERT_THAT(graph->GetNumNodes(), Eq(2));
+
+  // Even though doc0 and doc1 have join relation, since the delete propagation
+  // type of "softTarget" is NONE, it should not be included into the dependent
+  // edges.
+  ASSERT_THAT(
+      join_index_->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/0))));
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/0), IsOkAndHolds(IsEmpty()));
+}
+
+TEST_F(DocumentDependentGraphTest, GetEdgesIterator_shouldDedupeDocumentIds) {
+  // Put and index 2 documents with the following relations:
+  //
+  // doc0 -> doc1
+  //
+  // And doc0, doc1 can be joined by multiple joinable properties with delete
+  // propagation enabled.
+  DocumentProto doc0 =
+      DocumentBuilder().SetKey("namespace", "uri/0").SetSchema("Label").Build();
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .AddStringProperty("target2", "namespace#uri/0")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1}));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocumentDependentGraph> graph,
+      DocumentDependentGraph::Create(schema_store_.get(), doc_store_.get(),
+                                     join_index_.get()));
+  ASSERT_THAT(graph->GetNumNodes(), Eq(2));
+
+  // doc0 should have 2 join relations with doc1, but doc1 should be returned
+  // only once by the iterator.
+  ASSERT_THAT(
+      join_index_->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/1),
+          DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/2))));
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/0),
+              IsOkAndHolds(ElementsAre(1)));
+}
+
+TEST_F(DocumentDependentGraphTest,
+       GetEdgesIterator_replacedDocument_removeEdges) {
+  // Put and index 3 documents with the following relations:
+  //
+  // doc0 -> doc1 -> doc2
+  DocumentProto doc0 =
+      DocumentBuilder().SetKey("namespace", "uri/0").SetSchema("Label").Build();
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  DocumentProto doc2 = DocumentBuilder()
+                           .SetKey("namespace", "uri/2")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/1")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1, doc2}));
+
+  // Replace doc1 with new relations.
+  // doc0  doc1_replaced -> doc2
+  DocumentProto doc1_replaced =
+      DocumentBuilder().SetKey("namespace", "uri/1").SetSchema("Label").Build();
+  ICING_ASSERT_OK(AddDocuments({doc1_replaced}));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocumentDependentGraph> graph,
+      DocumentDependentGraph::Create(schema_store_.get(), doc_store_.get(),
+                                     join_index_.get()));
+  ASSERT_THAT(graph->GetNumNodes(), Eq(4));
+
+  // Sanity check: join indexing handler handled migrate parent, so doc id 1
+  // should be migrated to 3. Doc 0 should still contain the original relation
+  // with doc 1.
+  ASSERT_THAT(
+      join_index_->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/1))));
+  ASSERT_THAT(
+      join_index_->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/1),
+      IsOkAndHolds(IsEmpty()));
+  ASSERT_THAT(
+      join_index_->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/2),
+      IsOkAndHolds(IsEmpty()));
+  ASSERT_THAT(
+      join_index_->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/3),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(/*document_id=*/2, /*joinable_property_id=*/1))));
+
+  // Doc 0 should skip the edge to doc 1, since doc 1 was replaced.
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/0), IsOkAndHolds(IsEmpty()));
+  // Doc 1 should return empty iterator since it was replaced.
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/1), IsOkAndHolds(IsEmpty()));
+  // Doc 2 should remain the same.
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/2), IsOkAndHolds(IsEmpty()));
+  // Doc 3 should contain 2.
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/3),
+              IsOkAndHolds(ElementsAre(2)));
+}
+
+TEST_F(DocumentDependentGraphTest, GetEdgesIterator_replacedDocument_addEdges) {
+  // Put and index 4 documents with the following relations:
+  //
+  // doc0 -> doc1 -> doc2 -> doc3
+  DocumentProto doc0 =
+      DocumentBuilder().SetKey("namespace", "uri/0").SetSchema("Label").Build();
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  DocumentProto doc2 = DocumentBuilder()
+                           .SetKey("namespace", "uri/2")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/1")
+                           .Build();
+  DocumentProto doc3 = DocumentBuilder()
+                           .SetKey("namespace", "uri/3")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/2")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1, doc2, doc3}));
+
+  // Replace doc2 with new relations.
+  // doc0 -> doc1 -> doc2_replaced -> doc3
+  //   |                  ^
+  //   |                  |
+  //   +------------------+
+  DocumentProto doc2_replaced =
+      DocumentBuilder()
+          .SetKey("namespace", "uri/2")
+          .SetSchema("Label")
+          .AddStringProperty("target", "namespace#uri/1", "namespace#uri/0")
+          .Build();
+  ICING_ASSERT_OK(AddDocuments({doc2_replaced}));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocumentDependentGraph> graph,
+      DocumentDependentGraph::Create(schema_store_.get(), doc_store_.get(),
+                                     join_index_.get()));
+  ASSERT_THAT(graph->GetNumNodes(), Eq(5));
+
+  // Sanity check: join indexing handler handled migrate parent, so doc id 2
+  // should be migrated to 4. Doc 1 should still contain the original relation
+  // with doc 2.
+  ASSERT_THAT(
+      join_index_->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/1),
+          DocumentJoinIdPair(/*document_id=*/4, /*joinable_property_id=*/1))));
+  ASSERT_THAT(
+      join_index_->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/1),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(/*document_id=*/2, /*joinable_property_id=*/1),
+          DocumentJoinIdPair(/*document_id=*/4, /*joinable_property_id=*/1))));
+  ASSERT_THAT(
+      join_index_->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/2),
+      IsOkAndHolds(IsEmpty()));
+  ASSERT_THAT(
+      join_index_->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/3),
+      IsOkAndHolds(IsEmpty()));
+  ASSERT_THAT(
+      join_index_->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/4),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(/*document_id=*/3, /*joinable_property_id=*/1))));
+
+  // Doc 0 should contain both edges to doc 1 and doc 4.
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/0),
+              IsOkAndHolds(ElementsAre(1, 4)));
+  // Doc 1 should skip the edge to doc 2, since doc 2 was replaced.
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/1),
+              IsOkAndHolds(ElementsAre(4)));
+  // Doc 2 should return empty iterator since it was replaced.
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/2), IsOkAndHolds(IsEmpty()));
+  // Doc 3 should remain the same.
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/3), IsOkAndHolds(IsEmpty()));
+  // Doc 4 should contain 3.
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/4),
+              IsOkAndHolds(ElementsAre(3)));
+}
+
+TEST_F(DocumentDependentGraphTest,
+       GetEdgesIterator_deletedParentShouldReturnEmptyIterator) {
+  // Put and index 3 documents with the following relations:
+  //
+  // doc0 -> doc1
+  //  |
+  //  +----> doc2
+  DocumentProto doc0 =
+      DocumentBuilder().SetKey("namespace", "uri/0").SetSchema("Label").Build();
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  DocumentProto doc2 = DocumentBuilder()
+                           .SetKey("namespace", "uri/2")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1, doc2}));
+
+  // Delete doc0.
+  ICING_ASSERT_OK(doc_store_->Delete(
+      /*document_id=*/0,
+      /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocumentDependentGraph> graph,
+      DocumentDependentGraph::Create(schema_store_.get(), doc_store_.get(),
+                                     join_index_.get()));
+  ASSERT_THAT(graph->GetNumNodes(), Eq(3));
+
+  ASSERT_THAT(
+      join_index_->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/1),
+          DocumentJoinIdPair(/*document_id=*/2, /*joinable_property_id=*/1))));
+  // Since doc0 is deleted, the iterator should return empty.
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/0), IsOkAndHolds(IsEmpty()));
+}
+
+TEST_F(DocumentDependentGraphTest,
+       GetEdgesIterator_deletedChildShouldBeSkipped) {
+  // Put and index 3 documents with the following relations:
+  //
+  // doc0 -> doc1
+  //  |
+  //  +----> doc2
+  DocumentProto doc0 =
+      DocumentBuilder().SetKey("namespace", "uri/0").SetSchema("Label").Build();
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  DocumentProto doc2 = DocumentBuilder()
+                           .SetKey("namespace", "uri/2")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1, doc2}));
+
+  // Delete doc1.
+  ICING_ASSERT_OK(doc_store_->Delete(
+      /*document_id=*/1,
+      /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocumentDependentGraph> graph,
+      DocumentDependentGraph::Create(schema_store_.get(), doc_store_.get(),
+                                     join_index_.get()));
+  ASSERT_THAT(graph->GetNumNodes(), Eq(3));
+
+  ASSERT_THAT(
+      join_index_->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/1),
+          DocumentJoinIdPair(/*document_id=*/2, /*joinable_property_id=*/1))));
+  // Since doc1 is deleted, the iterator of doc0 should skip doc1.
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/0),
+              IsOkAndHolds(ElementsAre(2)));
+}
+
+TEST_F(DocumentDependentGraphTest,
+       GetEdgesIterator_expiredParentShouldNotBeSkipped) {
+  fake_clock_.SetSystemTimeMilliseconds(0);
+
+  // Put and index 3 documents with the following relations:
+  //
+  // doc0 -> doc1
+  //  |
+  //  +----> doc2
+  DocumentProto doc0 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(1000)
+                           .SetKey("namespace", "uri/0")
+                           .SetSchema("Label")
+                           .Build();
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  DocumentProto doc2 = DocumentBuilder()
+                           .SetKey("namespace", "uri/2")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1, doc2}));
+
+  // Adjust the clock to expire doc0.
+  fake_clock_.SetSystemTimeMilliseconds(2000);
+  ASSERT_THAT(doc_store_->GetAliveDocumentFilterData(
+                  /*document_id=*/0,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsFalse());
+  ASSERT_THAT(doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/0),
+              IsTrue());
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocumentDependentGraph> graph,
+      DocumentDependentGraph::Create(schema_store_.get(), doc_store_.get(),
+                                     join_index_.get()));
+  ASSERT_THAT(graph->GetNumNodes(), Eq(3));
+
+  ASSERT_THAT(
+      join_index_->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/1),
+          DocumentJoinIdPair(/*document_id=*/2, /*joinable_property_id=*/1))));
+  // We should still be able to get all of doc0's edges.
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/0),
+              IsOkAndHolds(ElementsAre(1, 2)));
+}
+
+TEST_F(DocumentDependentGraphTest,
+       GetEdgesIterator_expiredChildShouldNotBeSkipped) {
+  fake_clock_.SetSystemTimeMilliseconds(0);
+
+  // Put and index 3 documents with the following relations:
+  //
+  // doc0 -> doc1
+  //  |
+  //  +----> doc2
+  DocumentProto doc0 =
+      DocumentBuilder().SetKey("namespace", "uri/0").SetSchema("Label").Build();
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(1000)
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  DocumentProto doc2 = DocumentBuilder()
+                           .SetKey("namespace", "uri/2")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1, doc2}));
+
+  // Adjust the clock to expire doc1.
+  fake_clock_.SetSystemTimeMilliseconds(2000);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocumentDependentGraph> graph,
+      DocumentDependentGraph::Create(schema_store_.get(), doc_store_.get(),
+                                     join_index_.get()));
+  ASSERT_THAT(graph->GetNumNodes(), Eq(3));
+
+  ASSERT_THAT(
+      join_index_->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/1),
+          DocumentJoinIdPair(/*document_id=*/2, /*joinable_property_id=*/1))));
+  // We should still be able to get doc1 from doc0's edge iterator.
+  EXPECT_THAT(GetEdgesOfNode(*graph, /*node_id=*/0),
+              IsOkAndHolds(ElementsAre(1, 2)));
+}
+
+TEST_F(DocumentDependentGraphTest, GetEdgesIterator_invalidNodeIdShouldFail) {
+  ASSERT_THAT(doc_store_->last_added_document_id(), Eq(kInvalidDocumentId));
+  ASSERT_THAT(join_index_->last_added_document_id(), Eq(kInvalidDocumentId));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocumentDependentGraph> graph,
+      DocumentDependentGraph::Create(schema_store_.get(), doc_store_.get(),
+                                     join_index_.get()));
+
+  EXPECT_THAT(graph->GetNumNodes(), Eq(0));
+  EXPECT_THAT(graph->GetEdgesIterator(/*node_id=*/-2),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(graph->GetEdgesIterator(/*node_id=*/-1),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(graph->GetEdgesIterator(/*node_id=*/0),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(graph->GetEdgesIterator(/*node_id=*/1),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(graph->GetEdgesIterator(/*node_id=*/2),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(graph->GetEdgesIterator(/*node_id=*/kInvalidDocumentId),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+
+  // Put and index 2 documents.
+  DocumentProto doc0 =
+      DocumentBuilder().SetKey("namespace", "uri/0").SetSchema("Label").Build();
+  DocumentProto doc1 =
+      DocumentBuilder().SetKey("namespace", "uri/1").SetSchema("Label").Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1}));
+  ASSERT_THAT(doc_store_->last_added_document_id(), Eq(1));
+  ASSERT_THAT(join_index_->last_added_document_id(), Eq(1));
+
+  EXPECT_THAT(graph->GetNumNodes(), Eq(2));
+  EXPECT_THAT(graph->GetEdgesIterator(/*node_id=*/-2),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(graph->GetEdgesIterator(/*node_id=*/-1),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(graph->GetEdgesIterator(/*node_id=*/0), IsOkAndHolds(NotNull()));
+  EXPECT_THAT(graph->GetEdgesIterator(/*node_id=*/1), IsOkAndHolds(NotNull()));
+  EXPECT_THAT(graph->GetEdgesIterator(/*node_id=*/2),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(graph->GetEdgesIterator(/*node_id=*/kInvalidDocumentId),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/join/expiration-timestamp-util.cc b/icing/join/expiration-timestamp-util.cc
new file mode 100644
index 0000000..ed518d4
--- /dev/null
+++ b/icing/join/expiration-timestamp-util.cc
@@ -0,0 +1,159 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/join/expiration-timestamp-util.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <limits>
+#include <memory>
+#include <optional>
+#include <queue>
+#include <unordered_set>
+#include <utility>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/absl_ports/canonical_errors.h"
+#include "icing/graph/graph-interface.h"
+#include "icing/join/document-dependent-graph.h"
+#include "icing/join/qualified-id-join-index.h"
+#include "icing/schema/schema-store.h"
+#include "icing/store/document-filter-data.h"
+#include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
+#include "icing/util/logging.h"
+#include "icing/util/status-macros.h"
+
+namespace icing {
+namespace lib {
+
+/* static */ libtextclassifier3::Status
+ExpirationTimestampUtil::SingleDocumentPropagation(
+    DocumentId document_id,
+    const std::unordered_set<DocumentId>& dependency_doc_ids,
+    const SchemaStore& schema_store,
+    const QualifiedIdJoinIndex& qualified_id_join_index,
+    DocumentStore& document_store, int64_t current_time_ms) {
+  // This function implements the algorithm to:
+  // - Update the given document's expiration timestamp by its given
+  //   dependencies. Mostly its dependencies are determined by the dependency
+  //   evaluation. See DocumentDependencyProcessor for more details.
+  // - Propagate the expiration timestamp to all of the given document's
+  //   non-deleted dependents (and their dependents, and so on), based on the
+  //   dependent graph.
+  //
+  // For example:
+  // - dependency_doc_ids = {depcy1, depcy2}
+  // - dependent_graph contains the edges of the following graph.
+  //
+  //                 +--------> dept1 ------+
+  //                 |                      |
+  //                 |                      v
+  // depcy1 --> document_id --> dept2 --> dept3
+  //                 ^            |
+  //                 |            v
+  // depcy2 ---------+          dept4
+  //
+  // The algorithm will:
+  // - Update document_id's expiration timestamp by min(depcy1_exp_ts,
+  //   depcy2_exp_ts) if this min value is smaller than the document's current
+  //   expiration timestamp.
+  // - BFS traverse: propagate the expiration timestamp to dept1, dept2, dept3,
+  //   and dept4. Early stop if any of the dependents' expiration timestamp is
+  //   not smaller than the propagated value.
+
+  // Step 1.0: get the min expiration timestamp of the dependencies.
+  int64_t min_depcy_exp_ts_ms = std::numeric_limits<int64_t>::max();
+  for (DocumentId depcy_doc_id : dependency_doc_ids) {
+    std::optional<DocumentFilterData> doc_filter_data =
+        document_store.GetAliveDocumentFilterData(depcy_doc_id,
+                                                  current_time_ms);
+    if (!doc_filter_data.has_value()) {
+      // This really shouldn't happen since they were validated in the caller
+      // side by DocumentDependencyProcessor.
+      ICING_LOG(ERROR) << "A dependency document is not alive after dependency "
+                          "evaluation. This should never happen.";
+      return absl_ports::InternalError(
+          "A dependency document is not alive after dependency evaluation.");
+    }
+    min_depcy_exp_ts_ms = std::min(min_depcy_exp_ts_ms,
+                                   doc_filter_data->expiration_timestamp_ms());
+  }
+
+  // Step 1.1: update min expiration timestamp from dependencies to the
+  //           document.
+  //
+  // Note:
+  // - UpdateDocumentExpirationTimestamp only overwrites the new expiration
+  //   timestamp if the new value is smaller than the existing one, so it is
+  //   safe to call this method directly.
+  // - final_exp_ts is the final expiration timestamp of the document.
+  //   - If min_depcy_exp_ts_ms is smaller than the document's current (raw)
+  //     expiration timestamp, final_exp_ts will be min_depcy_exp_ts_ms.
+  //   - Otherwise, final_exp_ts will be the document's (raw) expiration
+  //     timestamp.
+  ICING_ASSIGN_OR_RETURN(
+      DocumentStore::UpdateDocumentExpirationTimestampResult update_result,
+      document_store.UpdateDocumentExpirationTimestamp(document_id,
+                                                       min_depcy_exp_ts_ms));
+  int64_t final_exp_ts = update_result.final_expiration_timestamp_ms;
+
+  // Step 2: run BFS to propagate final_exp_ts to all dependents. In most cases,
+  //         a new document won't have any dependents at this point. Only alive
+  //         document replacement will have dependents to update.
+  ICING_ASSIGN_OR_RETURN(
+      std::unique_ptr<DocumentDependentGraph> dependent_graph,
+      DocumentDependentGraph::Create(&schema_store, &document_store,
+                                     &qualified_id_join_index));
+
+  std::unordered_set<DocumentId> visited_doc_ids;
+  std::queue<DocumentId> que;
+  visited_doc_ids.insert(document_id);
+  que.push(document_id);
+  while (!que.empty()) {
+    DocumentId curr_doc_id = que.front();
+    que.pop();
+
+    ICING_ASSIGN_OR_RETURN(
+        std::unique_ptr<graph::GraphInterface<DocumentId>::EdgeIteratorIf> itr,
+        dependent_graph->GetEdgesIterator(curr_doc_id));
+    while (itr->Advance().ok()) {
+      DocumentId next_doc_id = itr->Get();
+      if (!visited_doc_ids.insert(next_doc_id).second) {
+        // Already visited.
+        continue;
+      }
+
+      // Update the next document's expiration timestamp. Push into the queue
+      // only if the expiration timestamp is updated.
+      //
+      // Note: it is safe to call UpdateDocumentExpirationTimestamp directly and
+      //   return if getting an error here, because DocumentDependentGraph
+      //   returns edges to non deleted documents and at this point next_doc_id
+      //   is guaranteed to be valid and non-deleted.
+      ICING_ASSIGN_OR_RETURN(
+          DocumentStore::UpdateDocumentExpirationTimestampResult
+              dependent_update_result,
+          document_store.UpdateDocumentExpirationTimestamp(next_doc_id,
+                                                           final_exp_ts));
+      if (dependent_update_result.was_updated) {
+        que.push(next_doc_id);
+      }
+    }
+  }
+  return libtextclassifier3::Status::OK;
+}
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/join/expiration-timestamp-util.h b/icing/join/expiration-timestamp-util.h
new file mode 100644
index 0000000..759772a
--- /dev/null
+++ b/icing/join/expiration-timestamp-util.h
@@ -0,0 +1,82 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef ICING_JOIN_EXPIRATION_TIMESTAMP_UTIL_H_
+#define ICING_JOIN_EXPIRATION_TIMESTAMP_UTIL_H_
+
+#include <cstdint>
+#include <unordered_set>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/join/qualified-id-join-index.h"
+#include "icing/schema/schema-store.h"
+#include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
+
+namespace icing {
+namespace lib {
+
+class ExpirationTimestampUtil {
+ public:
+  // Updates the given single document's expiration timestamp by its
+  // dependencies. Also propagates the expiration timestamp to all of its
+  // "non-deleted" dependents (and their dependents, and so on), based on the
+  // dependent graph stored in the join index.
+  //
+  // Note:
+  // - The implementation is designed for updating a single document in (single)
+  //   Put API.
+  // - To make it efficient, currently it only allows a smaller expiration
+  //   timestamp to be propagated down. IOW, if the new expiration timestamp is
+  //   larger than the existing one, the existing one will be kept and stop
+  //   propagation.
+  // - Therefore, we can run simple BFS instead of Bellman-Ford algorithm, and
+  //   the time complexity is better.
+  // - In the future, if we decide to support updating (1) multiple documents on
+  //   the subgraph for batch API (2) larger expiration timestamp, then:
+  //   - Running K times BFS (K = number of documents to update) is not
+  //     efficient.
+  //   - Bellman-Ford algorithm is not efficient enough either, especially when
+  //     there are cycles in the dependent graph.
+  //   - We should consider running the SCC + topological sort algorithm to
+  //     propagate the expiration timestamp in linear time complexity.
+  //   - For optimization, subgraph SCC and topological sort can be performed,
+  //     but this requires to store the reverse dependent edges for determining
+  //     the subgraph that requires updating.
+  //
+  // Parameters:
+  //   - document_id: the single document id to update.
+  //   - dependency_doc_ids: dependencies of the document_id.
+  //   - schema_store: the schema store.
+  //   - qualified_id_join_index: the qualified id join index.
+  //   - document_store.
+  //   - current_time_ms.
+  //
+  // Returns:
+  //   - OK on success, and the expiration timestamps (which are stored in
+  //     DocumentStore -> DocumentFilterData cache) of the document and its
+  //     dependents are updated.
+  //   - Any error from document store, schema store, or join index.
+  static libtextclassifier3::Status SingleDocumentPropagation(
+      DocumentId document_id,
+      const std::unordered_set<DocumentId>& dependency_doc_ids,
+      const SchemaStore& schema_store,
+      const QualifiedIdJoinIndex& qualified_id_join_index,
+      DocumentStore& document_store, int64_t current_time_ms);
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_JOIN_EXPIRATION_TIMESTAMP_UTIL_H_
diff --git a/icing/join/expiration-timestamp-util_test.cc b/icing/join/expiration-timestamp-util_test.cc
new file mode 100644
index 0000000..739a566
--- /dev/null
+++ b/icing/join/expiration-timestamp-util_test.cc
@@ -0,0 +1,948 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/join/expiration-timestamp-util.h"
+
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "icing/document-builder.h"
+#include "icing/feature-flags.h"
+#include "icing/file/filesystem.h"
+#include "icing/file/portable-file-backed-proto-log.h"
+#include "icing/join/qualified-id-join-index-impl-v3.h"
+#include "icing/join/qualified-id-join-indexing-handler.h"
+#include "icing/portable/gzip_stream.h"
+#include "icing/portable/platform.h"
+#include "icing/proto/document.pb.h"
+#include "icing/proto/document_wrapper.pb.h"
+#include "icing/proto/schema.pb.h"
+#include "icing/proto/scoring.pb.h"
+#include "icing/proto/search.pb.h"
+#include "icing/schema-builder.h"
+#include "icing/schema/schema-store.h"
+#include "icing/store/document-filter-data.h"
+#include "icing/store/document-store.h"
+#include "icing/testing/common-matchers.h"
+#include "icing/testing/fake-clock.h"
+#include "icing/testing/test-data.h"
+#include "icing/testing/test-feature-flags.h"
+#include "icing/testing/tmp-directory.h"
+#include "icing/tokenization/language-segmenter-factory.h"
+#include "icing/tokenization/language-segmenter.h"
+#include "icing/util/icu-data-file-helper.h"
+#include "icing/util/status-macros.h"
+#include "icing/util/tokenized-document.h"
+#include "unicode/uloc.h"
+
+namespace icing {
+namespace lib {
+
+namespace {
+
+using ::testing::Eq;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
+using ::testing::Optional;
+using ::testing::Property;
+
+class ExpirationTimestampUtilTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+    ASSERT_THAT(feature_flags_->enable_repeated_field_joins(), IsTrue());
+
+    test_dir_ = GetTestTempDir() + "/expiration_timestamp_util_test";
+    ASSERT_THAT(filesystem_.CreateDirectoryRecursively(test_dir_.c_str()),
+                IsTrue());
+
+    schema_store_dir_ = test_dir_ + "/schema_store";
+    doc_store_dir_ = test_dir_ + "/doc_store";
+    qualified_id_join_index_dir_ = test_dir_ + "/qualified_id_join_index";
+
+    if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
+      ICING_ASSERT_OK(
+          // File generated via icu_data_file rule in //icing/BUILD.
+          icu_data_file_helper::SetUpIcuDataFile(
+              GetTestFilePath("icing/icu.dat")));
+    }
+
+    language_segmenter_factory::SegmenterOptions options(ULOC_US);
+    ICING_ASSERT_OK_AND_ASSIGN(
+        lang_segmenter_,
+        language_segmenter_factory::Create(std::move(options)));
+
+    ASSERT_THAT(
+        filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str()),
+        IsTrue());
+    ICING_ASSERT_OK_AND_ASSIGN(
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
+
+    SchemaProto schema =
+        SchemaBuilder()
+            .AddType(SchemaTypeConfigBuilder().SetType("Label").AddProperty(
+                PropertyConfigBuilder()
+                    .SetName("target")
+                    .SetDataTypeJoinableString(
+                        JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                        DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                    .SetCardinality(CARDINALITY_REPEATED)))
+
+            .Build();
+    ASSERT_THAT(schema_store_->SetSchema(
+                    schema, /*ignore_errors_and_delete_documents=*/false),
+                IsOk());
+
+    ASSERT_THAT(filesystem_.CreateDirectoryRecursively(doc_store_dir_.c_str()),
+                IsTrue());
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::CreateResult create_result,
+        DocumentStore::Create(
+            &filesystem_, doc_store_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
+    doc_store_ = std::move(create_result.document_store);
+
+    ICING_ASSERT_OK_AND_ASSIGN(
+        qualified_id_join_index_,
+        QualifiedIdJoinIndexImplV3::Create(
+            filesystem_, qualified_id_join_index_dir_, *feature_flags_));
+
+    ICING_ASSERT_OK_AND_ASSIGN(
+        qualified_id_join_indexing_handler_,
+        QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
+                                               qualified_id_join_index_.get(),
+                                               feature_flags_.get()));
+  }
+
+  void TearDown() override {
+    qualified_id_join_indexing_handler_.reset();
+
+    qualified_id_join_index_.reset();
+    doc_store_.reset();
+    schema_store_.reset();
+    lang_segmenter_.reset();
+
+    filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
+  }
+
+  // Helper function to batch add documents.
+  libtextclassifier3::Status AddDocuments(
+      std::vector<DocumentProto> documents) {
+    // Tokenize all documents.
+    std::vector<TokenizedDocument> tokenized_documents;
+    tokenized_documents.reserve(documents.size());
+    for (DocumentProto& document : documents) {
+      ICING_ASSIGN_OR_RETURN(
+          TokenizedDocument tokenized_document,
+          TokenizedDocument::Create(
+              schema_store_.get(), lang_segmenter_.get(),
+              /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+              std::move(document)));
+      tokenized_documents.push_back(std::move(tokenized_document));
+    }
+
+    // Put all documents into the document store and get document ids.
+    std::vector<DocumentStore::PutResult> put_results;
+    put_results.reserve(documents.size());
+    for (const TokenizedDocument& tokenized_document : tokenized_documents) {
+      ICING_ASSIGN_OR_RETURN(
+          DocumentStore::PutResult put_result,
+          doc_store_->Put(tokenized_document.document_wrapper()));
+      put_results.push_back(std::move(put_result));
+    }
+
+    // Index all documents.
+    for (int i = 0; i < tokenized_documents.size(); ++i) {
+      ICING_RETURN_IF_ERROR(qualified_id_join_indexing_handler_->Handle(
+          tokenized_documents[i], put_results[i].new_document_id,
+          put_results[i].old_document_id, /*recovery_mode=*/false,
+          /*put_document_stats=*/nullptr));
+    }
+    return libtextclassifier3::Status::OK;
+  }
+
+  std::unique_ptr<FeatureFlags> feature_flags_;
+  Filesystem filesystem_;
+  FakeClock fake_clock_;
+  std::string test_dir_;
+  std::string schema_store_dir_;
+  std::string doc_store_dir_;
+  std::string qualified_id_join_index_dir_;
+
+  std::unique_ptr<LanguageSegmenter> lang_segmenter_;
+  std::unique_ptr<SchemaStore> schema_store_;
+  std::unique_ptr<DocumentStore> doc_store_;
+  std::unique_ptr<QualifiedIdJoinIndexImplV3> qualified_id_join_index_;
+
+  std::unique_ptr<QualifiedIdJoinIndexingHandler>
+      qualified_id_join_indexing_handler_;
+};
+
+TEST_F(ExpirationTimestampUtilTest, SingleDocumentPropagation) {
+  // General test for SingleDocumentPropagation.
+  fake_clock_.SetSystemTimeMilliseconds(0);
+
+  // Create docs with the following (raw) expiration timestamps and relations:
+  //
+  //                     +------> dept1 (30) -------+
+  //                     |                          |
+  //                     |                          v
+  // depcy1 (10) ---> doc (5) --> dept2 (30) --> dept3 (30)
+  //                     ^           |
+  //                     |           v
+  // depcy2 (2) ---------+        dept4 (30)
+  DocumentProto depcy1 = DocumentBuilder()
+                             .SetCreationTimestampMs(0)
+                             .SetTtlMs(10)
+                             .SetKey("namespace", "depcy1")
+                             .SetSchema("Label")
+                             .Build();
+  DocumentProto depcy2 = DocumentBuilder()
+                             .SetCreationTimestampMs(0)
+                             .SetTtlMs(2)
+                             .SetKey("namespace", "depcy2")
+                             .SetSchema("Label")
+                             .Build();
+  DocumentProto doc =
+      DocumentBuilder()
+          .SetCreationTimestampMs(0)
+          .SetTtlMs(5)
+          .SetKey("namespace", "doc")
+          .SetSchema("Label")
+          .AddStringProperty("target", "namespace#depcy1", "namespace#depcy2")
+          .Build();
+  DocumentProto dept1 = DocumentBuilder()
+                            .SetCreationTimestampMs(0)
+                            .SetTtlMs(30)
+                            .SetKey("namespace", "dept1")
+                            .SetSchema("Label")
+                            .AddStringProperty("target", "namespace#doc")
+                            .Build();
+  DocumentProto dept2 = DocumentBuilder()
+                            .SetCreationTimestampMs(0)
+                            .SetTtlMs(30)
+                            .SetKey("namespace", "dept2")
+                            .SetSchema("Label")
+                            .AddStringProperty("target", "namespace#doc")
+                            .Build();
+  DocumentProto dept3 =
+      DocumentBuilder()
+          .SetCreationTimestampMs(0)
+          .SetTtlMs(30)
+          .SetKey("namespace", "dept3")
+          .SetSchema("Label")
+          .AddStringProperty("target", "namespace#dept1", "namespace#dept2")
+          .Build();
+  DocumentProto dept4 = DocumentBuilder()
+                            .SetCreationTimestampMs(0)
+                            .SetTtlMs(30)
+                            .SetKey("namespace", "dept4")
+                            .SetSchema("Label")
+                            .AddStringProperty("target", "namespace#dept2")
+                            .Build();
+  // Add all documents. Note that they will have document ids 0, 1, 2, 3, 4,
+  // 5, 6.
+  ICING_ASSERT_OK(
+      AddDocuments({depcy1, depcy2, doc, dept1, dept2, dept3, dept4}));
+
+  // Run the expiration timestamp propagation on doc (id 2) with its
+  // dependencies depcy1 and depcy2 (id 0 and 1).
+  //
+  // The expiration timestamps should become:
+  //                     +------> dept1 (2) -------+
+  //                     |                         |
+  //                     |                         v
+  // depcy1 (10) ---> doc (2) --> dept2 (2) --> dept3 (2)
+  //                     ^           |
+  //                     |           v
+  // depcy2 (2) ---------+        dept4 (2)
+  EXPECT_THAT(ExpirationTimestampUtil::SingleDocumentPropagation(
+                  /*document_id=*/2, /*dependency_doc_ids=*/{0, 1},
+                  *schema_store_, *qualified_id_join_index_, *doc_store_,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsOk());
+
+  // depcy1 and depcy2 should not be updated.
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/0),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(10))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/1),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+
+  // Verify doc, dept1, dept2, dept3, and dept4's expiration timestamps are
+  // updated.
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/2),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/3),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/4),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/5),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/6),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+}
+
+TEST_F(
+    ExpirationTimestampUtilTest,
+    SingleDocumentPropagation_updateFromDependency_shouldUpdateToSmallerExpTs) {
+  fake_clock_.SetSystemTimeMilliseconds(0);
+
+  // Create doc0 and doc1 with raw expiration timestamps 10 and 2.
+  DocumentProto doc0 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(10)
+                           .SetKey("namespace", "uri/0")
+                           .SetSchema("Label")
+                           .Build();
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(2)
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1}));
+
+  // Create doc2 with raw expiration timestamp 5 and with the following
+  // relations:
+  //
+  // doc0 (10) --+
+  //             |
+  //             +---> doc2 (5)
+  //             |
+  // doc1 (2) ---+
+  DocumentProto doc2 =
+      DocumentBuilder()
+          .SetCreationTimestampMs(0)
+          .SetTtlMs(5)
+          .SetKey("namespace", "uri/2")
+          .SetSchema("Label")
+          .AddStringProperty("target", "namespace#uri/0", "namespace#uri/1")
+          .Build();
+  ICING_ASSERT_OK(AddDocuments({doc2}));
+
+  // Sanity check on the document filter data before propagation.
+  ASSERT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/0),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(10))));
+  ASSERT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/1),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+  ASSERT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/2),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(5))));
+
+  // Run the expiration timestamp propagation on doc2 with its dependencies doc0
+  // and doc1. Doc2's expiration timestamp should be updated to 2.
+  EXPECT_THAT(ExpirationTimestampUtil::SingleDocumentPropagation(
+                  /*document_id=*/2, /*dependency_doc_ids=*/{0, 1},
+                  *schema_store_, *qualified_id_join_index_, *doc_store_,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsOk());
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/2),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+}
+
+TEST_F(
+    ExpirationTimestampUtilTest,
+    SingleDocumentPropagation_updateFromDependency_shouldIgnoreGreaterExpTs) {
+  fake_clock_.SetSystemTimeMilliseconds(0);
+
+  // Create doc0 and doc1 with raw expiration timestamps 10 and 15.
+  DocumentProto doc0 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(10)
+                           .SetKey("namespace", "uri/0")
+                           .SetSchema("Label")
+                           .Build();
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(15)
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1}));
+
+  // Create doc2 with raw expiration timestamp 5 and with the following
+  // relations:
+  //
+  // doc0 (10) --+
+  //             |
+  //             +---> doc2 (5)
+  //             |
+  // doc1 (15) --+
+  DocumentProto doc2 =
+      DocumentBuilder()
+          .SetCreationTimestampMs(0)
+          .SetTtlMs(5)
+          .SetKey("namespace", "uri/2")
+          .SetSchema("Label")
+          .AddStringProperty("target", "namespace#uri/0", "namespace#uri/1")
+          .Build();
+  ICING_ASSERT_OK(AddDocuments({doc2}));
+
+  // Sanity check on the document filter data before propagation.
+  ASSERT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/0),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(10))));
+  ASSERT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/1),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(15))));
+  ASSERT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/2),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(5))));
+
+  // Run the expiration timestamp propagation on doc2 with its dependencies doc0
+  // and doc1. Since all of its dependencies have greater expiration timestamps,
+  // doc2's expiration timestamp should remain 5 after propagation.
+  EXPECT_THAT(ExpirationTimestampUtil::SingleDocumentPropagation(
+                  /*document_id=*/2, /*dependency_doc_ids=*/{0, 1},
+                  *schema_store_, *qualified_id_join_index_, *doc_store_,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsOk());
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/2),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(5))));
+}
+
+TEST_F(ExpirationTimestampUtilTest,
+       SingleDocumentPropagation_updateFromDependency_selfCycle) {
+  fake_clock_.SetSystemTimeMilliseconds(0);
+
+  // Create a document with a self cycle relation.
+  DocumentProto doc0 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(10)
+                           .SetKey("namespace", "uri/0")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .SetSchema("Label")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0}));
+
+  // Run the expiration timestamp propagation on doc0.
+  EXPECT_THAT(ExpirationTimestampUtil::SingleDocumentPropagation(
+                  /*document_id=*/0, /*dependency_doc_ids=*/{0}, *schema_store_,
+                  *qualified_id_join_index_, *doc_store_,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsOk());
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/0),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(10))));
+}
+
+TEST_F(ExpirationTimestampUtilTest,
+       SingleDocumentPropagation_propgateToDependents_smallerExpTs) {
+  // Note: in the actual integration with IcingSearchEngine, the only case to
+  //   propagate to dependent is when replacing an existing parent document, but
+  //   for testing purpose, we can just simply create all documents at once and
+  //   run the algorithm on the target document directly without creating
+  //   replacement scenario.
+  fake_clock_.SetSystemTimeMilliseconds(0);
+
+  // Create documents with the following (raw) expiration timestamps and
+  // relations:
+  //
+  // doc0 (2) ---> doc1 (5) ----> doc3 (1)
+  //    |
+  //    +--------> doc2 (10) ---> doc4 (3)
+  DocumentProto doc0 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(2)
+                           .SetKey("namespace", "uri/0")
+                           .SetSchema("Label")
+                           .Build();
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(5)
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  DocumentProto doc2 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(10)
+                           .SetKey("namespace", "uri/2")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  DocumentProto doc3 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(1)
+                           .SetKey("namespace", "uri/3")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/1")
+                           .Build();
+  DocumentProto doc4 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(3)
+                           .SetKey("namespace", "uri/4")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/2")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1, doc2, doc3, doc4}));
+
+  // Run the expiration timestamp propagation on doc0. It should propagate to
+  // doc1, doc2, and doc4.
+  //
+  // Final graph:
+  // doc0 (2) ----> doc1 (2) ----> doc3 (1)
+  //    |
+  //    +---------> doc2 (2) ----> doc4 (2)
+  EXPECT_THAT(ExpirationTimestampUtil::SingleDocumentPropagation(
+                  /*document_id=*/0, /*dependency_doc_ids=*/{}, *schema_store_,
+                  *qualified_id_join_index_, *doc_store_,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsOk());
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/0),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/1),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/2),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/3),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(1))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/4),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+}
+
+TEST_F(ExpirationTimestampUtilTest,
+       SingleDocumentPropagation_propgateToDependents_greaterExpTs) {
+  // Note: in the actual integration with IcingSearchEngine, the only case to
+  //   propagate to dependent is when replacing an existing parent document, but
+  //   for testing purpose, we can just simply create all documents at once and
+  //   run the algorithm on the target document directly without creating
+  //   replacement scenario.
+  fake_clock_.SetSystemTimeMilliseconds(0);
+
+  // Create documents with the following (raw) expiration timestamps and
+  // relations:
+  //
+  // doc0 (15) ---> doc1 (5) ----> doc3 (1)
+  //    |
+  //    +---------> doc2 (10) ---> doc4 (3)
+  DocumentProto doc0 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(15)
+                           .SetKey("namespace", "uri/0")
+                           .SetSchema("Label")
+                           .Build();
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(5)
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  DocumentProto doc2 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(10)
+                           .SetKey("namespace", "uri/2")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  DocumentProto doc3 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(1)
+                           .SetKey("namespace", "uri/3")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/1")
+                           .Build();
+  DocumentProto doc4 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(3)
+                           .SetKey("namespace", "uri/4")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/2")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1, doc2, doc3, doc4}));
+
+  // Run the expiration timestamp propagation on doc0. Since it has greater
+  // expiration timestamp than all of its dependents, no dependents' expiration
+  // timestamps should be updated.
+  //
+  // Final graph:
+  // doc0 (15) ---> doc1 (5) ----> doc3 (1)
+  //    |
+  //    +---------> doc2 (10) ---> doc4 (3)
+  EXPECT_THAT(ExpirationTimestampUtil::SingleDocumentPropagation(
+                  /*document_id=*/0, /*dependency_doc_ids=*/{}, *schema_store_,
+                  *qualified_id_join_index_, *doc_store_,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsOk());
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/0),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(15))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/1),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(5))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/2),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(10))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/3),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(1))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/4),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(3))));
+}
+
+TEST_F(ExpirationTimestampUtilTest,
+       SingleDocumentPropagation_propgateToDependents_expTsShouldOnlyDecrease) {
+  fake_clock_.SetSystemTimeMilliseconds(0);
+
+  // Create documents with the following raw expiration timestamps and
+  // relations:
+  //
+  // doc0 (raw 10) --+
+  //                 |
+  //                 +---> doc2 (raw 8)
+  //                 |
+  // doc1 (raw 5) ---+
+  DocumentProto doc0 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(10)
+                           .SetKey("namespace", "uri/0")
+                           .SetSchema("Label")
+                           .Build();
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(5)
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .Build();
+  DocumentProto doc2 =
+      DocumentBuilder()
+          .SetCreationTimestampMs(0)
+          .SetTtlMs(8)
+          .SetKey("namespace", "uri/2")
+          .SetSchema("Label")
+          .AddStringProperty("target", "namespace#uri/0", "namespace#uri/1")
+          .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1, doc2}));
+
+  // Run the expiration timestamp propagation on doc2. It should have
+  // (expiration ts, raw expiration ts) = (5, 8).
+  //
+  // doc0 (10, 10) --+
+  //                 |
+  //                 +---> doc2 (5, 8)
+  //                 |
+  // doc1 (5, 5) ----+
+  ASSERT_THAT(ExpirationTimestampUtil::SingleDocumentPropagation(
+                  /*document_id=*/2, /*dependency_doc_ids=*/{0, 1},
+                  *schema_store_, *qualified_id_join_index_, *doc_store_,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsOk());
+  std::optional<DocumentFilterData> filter_data =
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/2);
+  ASSERT_THAT(filter_data->expiration_timestamp_ms(), Eq(5));
+  ASSERT_THAT(filter_data->raw_expiration_timestamp_ms(), Eq(8));
+
+  // Create doc3 with raw expiration timestamp 7 and the same key as doc1.
+  // Use it to replace doc1.
+  DocumentProto doc3 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(7)
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc3}));
+
+  // Run the expiration timestamp propagation on doc3.
+  //
+  // doc0 (10, 10) --+
+  //                 |
+  //                 +---> doc2 (_, 8)
+  //                 |
+  // doc3 (7, 7) ----+
+  //
+  // According to the graph, if doc2 had been added after doc0 and doc3, then it
+  // should've had expiration timestamp 7 after propagation. But since doc3 is a
+  // replacement update now and the BFS algorithm only decreases the expiration
+  // timestamp of doc2 without considering potential increase caused by
+  // dependency replacement, doc2 should still have expiration timestamp 5.
+  EXPECT_THAT(ExpirationTimestampUtil::SingleDocumentPropagation(
+                  /*document_id=*/3, /*dependency_doc_ids=*/{}, *schema_store_,
+                  *qualified_id_join_index_, *doc_store_,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsOk());
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/2),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(5))));
+}
+
+TEST_F(ExpirationTimestampUtilTest,
+       SingleDocumentPropagation_propgateToDependents_cycle) {
+  // Note: in the actual integration with IcingSearchEngine, the only case to
+  //   propagate to dependent is when replacing an existing parent document, but
+  //   for testing purpose, we can just simply create all documents at once and
+  //   run the algorithm on the target document directly without creating
+  //   replacement scenario.
+  fake_clock_.SetSystemTimeMilliseconds(0);
+
+  // Create documents with the following raw expiration timestamps and
+  // relations:
+  //
+  // doc0 (raw 2) ----> doc1 (raw 5)
+  //    ^                   |
+  //    |                   |
+  //    |                   v
+  // doc3 (raw 7) <---- doc2 (raw 8)
+  DocumentProto doc0 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(2)
+                           .SetKey("namespace", "uri/0")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/3")
+                           .Build();
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(5)
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  DocumentProto doc2 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(8)
+                           .SetKey("namespace", "uri/2")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/1")
+                           .Build();
+  DocumentProto doc3 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(7)
+                           .SetKey("namespace", "uri/3")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/2")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1, doc2, doc3}));
+
+  // Run the expiration timestamp propagation on doc0.
+  EXPECT_THAT(ExpirationTimestampUtil::SingleDocumentPropagation(
+                  /*document_id=*/0, /*dependency_doc_ids=*/{3}, *schema_store_,
+                  *qualified_id_join_index_, *doc_store_,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsOk());
+
+  // All of them should have expiration timestamp 2.
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/0),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/1),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/2),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/3),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+}
+
+TEST_F(ExpirationTimestampUtilTest,
+       SingleDocumentPropagation_propgateToDependents_shouldSkipDeleted) {
+  fake_clock_.SetSystemTimeMilliseconds(0);
+
+  // Create documents with the following (raw) expiration timestamps and
+  // relations:
+  //
+  // doc0 (5) ---> doc1 (10)
+  DocumentProto doc0 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(5)
+                           .SetKey("namespace", "uri/0")
+                           .SetSchema("Label")
+                           .Build();
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(10)
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1}));
+
+  // Delete doc1.
+  ICING_ASSERT_OK(doc_store_->Delete(
+      /*document_id=*/1,
+      /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+
+  // Run the expiration timestamp propagation on doc0. doc1 should be skipped
+  // since it's deleted.
+  EXPECT_THAT(ExpirationTimestampUtil::SingleDocumentPropagation(
+                  /*document_id=*/0, /*dependency_doc_ids=*/{}, *schema_store_,
+                  *qualified_id_join_index_, *doc_store_,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsOk());
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/0),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(5))));
+  EXPECT_THAT(doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/1),
+              IsFalse());
+}
+
+TEST_F(ExpirationTimestampUtilTest,
+       SingleDocumentPropagation_propgateToDependents_shouldSkipExpired) {
+  fake_clock_.SetSystemTimeMilliseconds(0);
+
+  // Create documents with the following (raw) expiration timestamps and
+  // relations:
+  //
+  // doc0 (30) ---> doc1 (10)
+  DocumentProto doc0 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(30)
+                           .SetKey("namespace", "uri/0")
+                           .SetSchema("Label")
+                           .Build();
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(10)
+                           .SetKey("namespace", "uri/1")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/0")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1}));
+
+  // Adjust the current time to 20. It makes doc1 expired.
+  fake_clock_.SetSystemTimeMilliseconds(20);
+
+  // Run the expiration timestamp propagation on doc0. doc1 is still traversed
+  // and we attempt to set the propagated expiration timestamp, but the new
+  // value is always larger than the existing one, so it's skipped.
+  EXPECT_THAT(ExpirationTimestampUtil::SingleDocumentPropagation(
+                  /*document_id=*/0, /*dependency_doc_ids=*/{}, *schema_store_,
+                  *qualified_id_join_index_, *doc_store_,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsOk());
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/0),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(30))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/1),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(10))));
+}
+
+TEST_F(ExpirationTimestampUtilTest,
+       SingleDocumentPropagation_propgateToDependents_noBellmanFord) {
+  // Note: in the actual integration with IcingSearchEngine, this update
+  //   scenario is not possible. But for testing purpose, we want to make sure
+  //   the algorithm doesn't traverse nodes on a cycle for multiple times, which
+  //   has bad time complexity of O(V*E).
+  fake_clock_.SetSystemTimeMilliseconds(0);
+
+  // Create documents with the following raw expiration timestamps and
+  // relations:
+  //
+  // doc0 (raw 4) ----> doc1 (raw 8) <---------+
+  //                         |                 |
+  //                         |                 |
+  //                         v                 |
+  //                    doc2 (raw 5) ---> doc3 (raw 2)
+  DocumentProto doc0 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(4)
+                           .SetKey("namespace", "uri/0")
+                           .SetSchema("Label")
+                           .Build();
+  DocumentProto doc1 =
+      DocumentBuilder()
+          .SetCreationTimestampMs(0)
+          .SetTtlMs(8)
+          .SetKey("namespace", "uri/1")
+          .SetSchema("Label")
+          .AddStringProperty("target", "namespace#uri/0", "namespace#uri/3")
+          .Build();
+  DocumentProto doc2 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(5)
+                           .SetKey("namespace", "uri/2")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/1")
+                           .Build();
+  DocumentProto doc3 = DocumentBuilder()
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(2)
+                           .SetKey("namespace", "uri/3")
+                           .SetSchema("Label")
+                           .AddStringProperty("target", "namespace#uri/2")
+                           .Build();
+  ICING_ASSERT_OK(AddDocuments({doc0, doc1, doc2, doc3}));
+
+  // Run the expiration timestamp propagation on doc0.
+  EXPECT_THAT(ExpirationTimestampUtil::SingleDocumentPropagation(
+                  /*document_id=*/0, /*dependency_doc_ids=*/{}, *schema_store_,
+                  *qualified_id_join_index_, *doc_store_,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsOk());
+
+  // - According to the definition of the dependency, doc1, doc2, and doc3
+  //   should've had expiration timestamp 2.
+  // - But here, we're not running Bellman-Ford algorithm, so when starting from
+  //   doc0, the expiration timestamp of doc3 is not propagated. So doc3 should
+  //   remain 2, and doc1 and doc2 should be updated to 4.
+  //
+  // Note: in reality, when we added doc3, another round of propagation
+  //   should've propagated doc3's expiration timestamp to others, so we will
+  //   never get this incorrect scenario in production. If we really need batch
+  //   update for doc0 and doc3, then we should consider SCC + topological sort
+  //   (linear) algorithm, as mentioned in the docstring of
+  //   SingleDocumentPropagation.
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/0),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(4))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/1),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(4))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/2),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(4))));
+  EXPECT_THAT(
+      doc_store_->GetNonDeletedDocumentFilterData(/*document_id=*/3),
+      Optional(Property(&DocumentFilterData::expiration_timestamp_ms, Eq(2))));
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/join/join-children-fetcher-impl-v3.cc b/icing/join/join-children-fetcher-impl-v3.cc
index 456c3ba..52a6373 100644
--- a/icing/join/join-children-fetcher-impl-v3.cc
+++ b/icing/join/join-children-fetcher-impl-v3.cc
@@ -114,8 +114,9 @@
   // Otherwise, fetch the children from the qualified id join index and cache
   // the result.
   ICING_ASSIGN_OR_RETURN(
-      std::vector<DocumentJoinIdPair> child_document_join_id_pairs,
-      qualified_id_join_index_.Get(parent_doc_id));
+      QualifiedIdJoinIndex::DocumentJoinIdPairArrayView
+          child_join_id_pairs_array_view,
+      qualified_id_join_index_.GetDocumentJoinIdPairArrayView(parent_doc_id));
 
   // Filter and construct child_scored_document_hits for the given parent doc
   // id.
@@ -140,10 +141,10 @@
   //     set, or the joinable property does not match the one specified in the
   //     join spec.
   std::vector<ScoredDocumentHit> child_scored_document_hits;
-  for (const DocumentJoinIdPair& child_document_join_id_pair :
-       child_document_join_id_pairs) {
+  for (const DocumentJoinIdPair& child_join_id_pair :
+       child_join_id_pairs_array_view) {
     if (auto filter_itr = child_join_id_pair_to_scored_document_hit_map_.find(
-            child_document_join_id_pair);
+            child_join_id_pair);
         filter_itr != child_join_id_pair_to_scored_document_hit_map_.end()) {
       child_scored_document_hits.push_back(filter_itr->second);
     }
diff --git a/icing/join/join-children-fetcher-impl-v3_test.cc b/icing/join/join-children-fetcher-impl-v3_test.cc
index 9a39157..8cc13e5 100644
--- a/icing/join/join-children-fetcher-impl-v3_test.cc
+++ b/icing/join/join-children-fetcher-impl-v3_test.cc
@@ -32,6 +32,7 @@
 #include "icing/join/qualified-id-join-index-impl-v2.h"
 #include "icing/join/qualified-id-join-index-impl-v3.h"
 #include "icing/join/qualified-id-join-indexing-handler.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/document_wrapper.pb.h"
@@ -69,6 +70,8 @@
  protected:
   void SetUp() override {
     feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+    fake_clock_.SetSystemTimeMilliseconds(123);
+
     base_dir_ = GetTestTempDir() + "/icing_test";
     ASSERT_THAT(filesystem_.CreateDirectoryRecursively(base_dir_.c_str()),
                 IsTrue());
@@ -131,14 +134,18 @@
                 IsTrue());
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, doc_store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, doc_store_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     doc_store_ = std::move(create_result.document_store);
 
     ICING_ASSERT_OK_AND_ASSIGN(
@@ -158,17 +165,21 @@
 
   libtextclassifier3::StatusOr<DocumentId> PutAndIndexDocument(
       const DocumentProto& document) {
-    ICING_ASSIGN_OR_RETURN(DocumentStore::PutResult put_result,
-                           doc_store_->Put(document));
     ICING_ASSIGN_OR_RETURN(
         TokenizedDocument tokenized_document,
-        TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                  document));
+        TokenizedDocument::Create(
+            schema_store_.get(), lang_segmenter_.get(),
+            /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+            std::move(document)));
+    ICING_ASSIGN_OR_RETURN(
+        DocumentStore::PutResult put_result,
+        doc_store_->Put(tokenized_document.document_wrapper()));
 
     ICING_ASSIGN_OR_RETURN(
         std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
         QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                               qualified_id_join_index_.get()));
+                                               qualified_id_join_index_.get(),
+                                               feature_flags_.get()));
     ICING_RETURN_IF_ERROR(
         handler->Handle(tokenized_document, put_result.new_document_id,
                         put_result.old_document_id, /*recovery_mode=*/false,
@@ -489,12 +500,14 @@
   ICING_ASSERT_OK_AND_ASSIGN(DocumentId message3_id,
                              PutAndIndexDocument(message3));
   // Sanity check for the join index.
-  ASSERT_THAT(qualified_id_join_index_->Get(person1_id),
-              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(message1_id, 0),
-                                       DocumentJoinIdPair(message2_id, 1),
-                                       DocumentJoinIdPair(message3_id, 0))));
-  ASSERT_THAT(qualified_id_join_index_->Get(person2_id),
-              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(message2_id, 0))));
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(person1_id),
+      IsOkAndHolds(ElementsAre(DocumentJoinIdPair(message1_id, 0),
+                               DocumentJoinIdPair(message2_id, 1),
+                               DocumentJoinIdPair(message3_id, 0))));
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(person2_id),
+      IsOkAndHolds(ElementsAre(DocumentJoinIdPair(message2_id, 0))));
 
   ScoredDocumentHit scored_doc_hit_message1(message1_id, kSectionIdMaskNone,
                                             /*score=*/1.0);
@@ -596,13 +609,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(DocumentId email3_id, PutAndIndexDocument(email3));
   ICING_ASSERT_OK_AND_ASSIGN(DocumentId email4_id, PutAndIndexDocument(email4));
   // Sanity check for the join index.
-  ASSERT_THAT(qualified_id_join_index_->Get(person1_id),
-              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(email1_id, 0),
-                                       DocumentJoinIdPair(email2_id, 0))));
-  ASSERT_THAT(qualified_id_join_index_->Get(person2_id),
-              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(email3_id, 0))));
-  ASSERT_THAT(qualified_id_join_index_->Get(person3_id),
-              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(email4_id, 0))));
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(person1_id),
+      IsOkAndHolds(ElementsAre(DocumentJoinIdPair(email1_id, 0),
+                               DocumentJoinIdPair(email2_id, 0))));
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(person2_id),
+      IsOkAndHolds(ElementsAre(DocumentJoinIdPair(email3_id, 0))));
+  ASSERT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(person3_id),
+      IsOkAndHolds(ElementsAre(DocumentJoinIdPair(email4_id, 0))));
 
   ScoredDocumentHit scored_doc_hit_email1(email1_id, kSectionIdMaskNone,
                                           /*score=*/1.0);
diff --git a/icing/join/join-processor.cc b/icing/join/join-processor.cc
index 9b6aa2a..00efc91 100644
--- a/icing/join/join-processor.cc
+++ b/icing/join/join-processor.cc
@@ -15,14 +15,10 @@
 #include "icing/join/join-processor.h"
 
 #include <algorithm>
-#include <deque>
 #include <memory>
 #include <optional>
-#include <queue>
-#include <string>
 #include <string_view>
 #include <unordered_map>
-#include <unordered_set>
 #include <utility>
 #include <vector>
 
@@ -31,12 +27,10 @@
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/absl_ports/str_cat.h"
 #include "icing/join/aggregation-scorer.h"
-#include "icing/join/document-join-id-pair.h"
 #include "icing/join/join-children-fetcher-impl-deprecated.h"
 #include "icing/join/join-children-fetcher-impl-v3.h"
 #include "icing/join/join-children-fetcher.h"
 #include "icing/join/qualified-id-join-index.h"
-#include "icing/join/qualified-id.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/proto/scoring.pb.h"
 #include "icing/proto/search.pb.h"
@@ -45,7 +39,6 @@
 #include "icing/store/document-filter-data.h"
 #include "icing/store/document-id.h"
 #include "icing/store/namespace-id-fingerprint.h"
-#include "icing/util/logging.h"
 #include "icing/util/status-macros.h"
 
 namespace icing {
@@ -209,80 +202,5 @@
   return joined_scored_document_hits;
 }
 
-libtextclassifier3::StatusOr<std::unordered_set<DocumentId>>
-JoinProcessor::GetPropagatedChildDocumentsToDelete(
-    const std::unordered_set<DocumentId>& deleted_document_ids) {
-  // Sanity check: join index should be V3.
-  if (qualified_id_join_index_->version() !=
-      QualifiedIdJoinIndex::Version::kV3) {
-    return absl_ports::UnimplementedError(
-        "QualifiedIdJoinIndex version must be V3 to support delete "
-        "propagation.");
-  }
-
-  // BFS traverse to find all child documents to propagate delete.
-  std::queue<DocumentId> que(
-      std::deque(deleted_document_ids.begin(), deleted_document_ids.end()));
-  std::unordered_set<DocumentId> child_documents_to_delete;
-  while (!que.empty()) {
-    DocumentId doc_id_to_expand = que.front();
-    que.pop();
-
-    ICING_ASSIGN_OR_RETURN(std::vector<DocumentJoinIdPair> child_join_id_pairs,
-                           qualified_id_join_index_->Get(doc_id_to_expand));
-    for (const DocumentJoinIdPair& child_join_id_pair : child_join_id_pairs) {
-      if (child_documents_to_delete.find(child_join_id_pair.document_id()) !=
-              child_documents_to_delete.end() ||
-          deleted_document_ids.find(child_join_id_pair.document_id()) !=
-              deleted_document_ids.end()) {
-        // Already added into the set to delete or already deleted (happens only
-        // when there is a cycle back to the deleted or traversed document in
-        // the join relation). Skip it.
-        continue;
-      }
-
-      // Get DocumentFilterData of the child document to look up its schema type
-      // id.
-      // - Skip if the child document has been deleted, since delete propagation
-      //   should've been done to all its children when deleting it previously.
-      // - Otherwise, we have to handle this child document and propagate delete
-      //   to the grandchildren, even if it is expired.
-      std::optional<DocumentFilterData> child_filter_data =
-          doc_store_->GetNonDeletedDocumentFilterData(
-              child_join_id_pair.document_id());
-      if (!child_filter_data) {
-        // The child document has been deleted. Skip.
-        continue;
-      }
-
-      libtextclassifier3::StatusOr<const JoinablePropertyMetadata*>
-          metadata_or = schema_store_->GetJoinablePropertyMetadata(
-              child_filter_data->schema_type_id(),
-              child_join_id_pair.joinable_property_id());
-      if (!metadata_or.ok() || metadata_or.ValueOrDie() == nullptr) {
-        // This shouldn't happen because we've validated it during indexing and
-        // only put valid DocumentJoinIdPair into qualified id join index.
-        // Log and skip it.
-        ICING_LOG(ERROR) << "Failed to get metadata for schema type id "
-                         << child_filter_data->schema_type_id()
-                         << ", joinable property id "
-                         << static_cast<int>(
-                                child_join_id_pair.joinable_property_id());
-        continue;
-      }
-      const JoinablePropertyMetadata* metadata = metadata_or.ValueOrDie();
-
-      if (metadata->value_type == JoinableConfig::ValueType::QUALIFIED_ID &&
-          metadata->delete_propagation_type ==
-              JoinableConfig::DeletePropagationType::PROPAGATE_FROM) {
-        child_documents_to_delete.insert(child_join_id_pair.document_id());
-        que.push(child_join_id_pair.document_id());
-      }
-    }
-  }
-
-  return child_documents_to_delete;
-}
-
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/join/join-processor.h b/icing/join/join-processor.h
index aa74395..11af2f3 100644
--- a/icing/join/join-processor.h
+++ b/icing/join/join-processor.h
@@ -17,19 +17,15 @@
 
 #include <cstdint>
 #include <memory>
-#include <string>
 #include <string_view>
-#include <unordered_set>
 #include <vector>
 
-#include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/join/join-children-fetcher.h"
 #include "icing/join/qualified-id-join-index.h"
 #include "icing/proto/search.pb.h"
 #include "icing/schema/schema-store.h"
 #include "icing/scoring/scored-document-hit.h"
-#include "icing/store/document-id.h"
 #include "icing/store/document-store.h"
 
 namespace icing {
@@ -65,16 +61,6 @@
       std::vector<ScoredDocumentHit>&& parent_scored_document_hits,
       const JoinChildrenFetcher& join_children_fetcher);
 
-  // Gets all child documents to delete, propagated from the given deleted
-  // documents.
-  //
-  // Returns:
-  //   - On success, a set of child document ids to delete.
-  //   - Any other errors.
-  libtextclassifier3::StatusOr<std::unordered_set<DocumentId>>
-  GetPropagatedChildDocumentsToDelete(
-      const std::unordered_set<DocumentId>& deleted_document_ids);
-
  private:
   // TODO(b/275121148): deprecate v2 after rollout v3.
 
diff --git a/icing/join/join-processor_test.cc b/icing/join/join-processor_test.cc
index dd88bd1..8fb4b80 100644
--- a/icing/join/join-processor_test.cc
+++ b/icing/join/join-processor_test.cc
@@ -14,15 +14,11 @@
 
 #include "icing/join/join-processor.h"
 
-#include <cstdint>
 #include <memory>
-#include <optional>
 #include <string>
-#include <unordered_set>
 #include <utility>
 #include <vector>
 
-#include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
@@ -31,12 +27,12 @@
 #include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
-#include "icing/join/document-join-id-pair.h"
 #include "icing/join/join-children-fetcher.h"
 #include "icing/join/qualified-id-join-index-impl-v2.h"
 #include "icing/join/qualified-id-join-index-impl-v3.h"
 #include "icing/join/qualified-id-join-index.h"
 #include "icing/join/qualified-id-join-indexing-handler.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/document_wrapper.pb.h"
@@ -83,6 +79,8 @@
 
   void SetUp() override {
     feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+    fake_clock_.SetSystemTimeMilliseconds(123);
+
     test_dir_ = GetTestTempDir() + "/icing_join_processor_test";
     ASSERT_THAT(filesystem_.CreateDirectoryRecursively(test_dir_.c_str()),
                 IsTrue());
@@ -186,14 +184,18 @@
                 IsTrue());
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, doc_store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, doc_store_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     doc_store_ = std::move(create_result.document_store);
 
     ICING_ASSERT_OK_AND_ASSIGN(qualified_id_join_index_,
@@ -230,18 +232,22 @@
   }
 
   libtextclassifier3::StatusOr<DocumentId> PutAndIndexDocument(
-      const DocumentProto& document) {
-    ICING_ASSIGN_OR_RETURN(DocumentStore::PutResult put_result,
-                           doc_store_->Put(document));
+      DocumentProto document) {
     ICING_ASSIGN_OR_RETURN(
         TokenizedDocument tokenized_document,
-        TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                  document));
+        TokenizedDocument::Create(
+            schema_store_.get(), lang_segmenter_.get(),
+            /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+            std::move(document)));
+    ICING_ASSIGN_OR_RETURN(
+        DocumentStore::PutResult put_result,
+        doc_store_->Put(tokenized_document.document_wrapper()));
 
     ICING_ASSIGN_OR_RETURN(
         std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
         QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                               qualified_id_join_index_.get()));
+                                               qualified_id_join_index_.get(),
+                                               feature_flags_.get()));
     ICING_RETURN_IF_ERROR(
         handler->Handle(tokenized_document, put_result.new_document_id,
                         put_result.old_document_id, /*recovery_mode=*/false,
@@ -963,679 +969,6 @@
                       /*child_scored_document_hits=*/{scored_doc_hit6}))));
 }
 
-TYPED_TEST(JoinProcessorTest, GetPropagatedChildDocumentsToDelete) {
-  DocumentProto person = DocumentBuilder()
-                             .SetKey("pkg$db/namespace", "person")
-                             .SetSchema("Person")
-                             .AddStringProperty("Name", "Alice")
-                             .Build();
-
-  // "sender" (joinable property id 2) has
-  // DELETE_PROPAGATION_TYPE_PROPAGATE_FROM.
-  DocumentProto message =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "message")
-          .SetSchema("Message")
-          .AddStringProperty("content", "test content")
-          .AddStringProperty("sender", "pkg$db/namespace#person")
-          .Build();
-
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person_doc_id,
-                             this->PutAndIndexDocument(person));
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message_doc_id,
-                             this->PutAndIndexDocument(message));
-
-  JoinProcessor join_processor(
-      this->doc_store_.get(), this->schema_store_.get(),
-      this->qualified_id_join_index_.get(),
-      /*current_time_ms=*/this->fake_clock_.GetSystemTimeMilliseconds());
-
-  if (!std::is_same_v<typename TestFixture::JOIN_INDEX_TYPE,
-                      QualifiedIdJoinIndexImplV3>) {
-    EXPECT_THAT(
-        join_processor.GetPropagatedChildDocumentsToDelete({person_doc_id}),
-        StatusIs(libtextclassifier3::StatusCode::UNIMPLEMENTED,
-                 HasSubstr("QualifiedIdJoinIndex version must be V3 to "
-                           "support delete propagation")));
-  } else {
-    // Sanity check.
-    ASSERT_THAT(this->qualified_id_join_index_->Get(person_doc_id),
-                IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
-                    message_doc_id, /*joinable_property_id=*/2))));
-
-    // Deleting the parent document should propagate the delete to its child
-    // document via the joinable property with
-    // DELETE_PROPAGATION_TYPE_PROPAGATE_FROM.
-    EXPECT_THAT(
-        join_processor.GetPropagatedChildDocumentsToDelete({person_doc_id}),
-        IsOkAndHolds(UnorderedElementsAre(message_doc_id)));
-  }
-}
-
-TYPED_TEST(
-    JoinProcessorTest,
-    GetPropagatedChildDocumentsToDelete_shouldNotPropagateToChildDocumentsWithPropagateDeleteDisabled) {
-  if (!std::is_same_v<typename TestFixture::JOIN_INDEX_TYPE,
-                      QualifiedIdJoinIndexImplV3>) {
-    GTEST_SKIP() << "Skipping test for non v3 QualifiedIdJoinIndex";
-  }
-
-  DocumentProto person = DocumentBuilder()
-                             .SetKey("pkg$db/namespace", "person")
-                             .SetSchema("Person")
-                             .AddStringProperty("Name", "Alice")
-                             .Build();
-
-  // "receiver" (joinable property id 0) has DELETE_PROPAGATION_TYPE_NONE.
-  DocumentProto message =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "message")
-          .SetSchema("Message")
-          .AddStringProperty("content", "test content")
-          .AddStringProperty("receiver", "pkg$db/namespace#person")
-          .Build();
-
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person_doc_id,
-                             this->PutAndIndexDocument(person));
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message_doc_id,
-                             this->PutAndIndexDocument(message));
-
-  // Sanity check.
-  ASSERT_THAT(this->qualified_id_join_index_->Get(person_doc_id),
-              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
-                  message_doc_id, /*joinable_property_id=*/0))));
-
-  JoinProcessor join_processor(
-      this->doc_store_.get(), this->schema_store_.get(),
-      this->qualified_id_join_index_.get(),
-      /*current_time_ms=*/this->fake_clock_.GetSystemTimeMilliseconds());
-
-  // Deleting the parent document should not propagate the delete to its child
-  // document via the joinable property with DELETE_PROPAGATION_TYPE_NONE.
-  EXPECT_THAT(
-      join_processor.GetPropagatedChildDocumentsToDelete({person_doc_id}),
-      IsOkAndHolds(IsEmpty()));
-}
-
-TYPED_TEST(
-    JoinProcessorTest,
-    GetPropagatedChildDocumentsToDelete_shouldNotPropagateToNonJoinableChildDocuments) {
-  if (!std::is_same_v<typename TestFixture::JOIN_INDEX_TYPE,
-                      QualifiedIdJoinIndexImplV3>) {
-    GTEST_SKIP() << "Skipping test for non v3 QualifiedIdJoinIndex";
-  }
-
-  DocumentProto person = DocumentBuilder()
-                             .SetKey("pkg$db/namespace", "person")
-                             .SetSchema("Person")
-                             .AddStringProperty("Name", "Alice")
-                             .Build();
-
-  // Put person's qualified id string in a non-joinable property.
-  DocumentProto message =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "message")
-          .SetSchema("Message")
-          .AddStringProperty("content", "pkg$db/namespace#person")
-          .Build();
-
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person_doc_id,
-                             this->PutAndIndexDocument(person));
-  ICING_ASSERT_OK(this->PutAndIndexDocument(message));
-
-  // Sanity check.
-  ASSERT_THAT(this->qualified_id_join_index_->Get(person_doc_id),
-              IsOkAndHolds(IsEmpty()));
-
-  JoinProcessor join_processor(
-      this->doc_store_.get(), this->schema_store_.get(),
-      this->qualified_id_join_index_.get(),
-      /*current_time_ms=*/this->fake_clock_.GetSystemTimeMilliseconds());
-
-  // Deleting the parent document should not propagate the delete to
-  // non-joinable child documents.
-  EXPECT_THAT(
-      join_processor.GetPropagatedChildDocumentsToDelete({person_doc_id}),
-      IsOkAndHolds(IsEmpty()));
-}
-
-TYPED_TEST(JoinProcessorTest,
-           GetPropagatedChildDocumentsToDelete_propagateViaMultipleProperties) {
-  if (!std::is_same_v<typename TestFixture::JOIN_INDEX_TYPE,
-                      QualifiedIdJoinIndexImplV3>) {
-    GTEST_SKIP() << "Skipping test for non v3 QualifiedIdJoinIndex";
-  }
-
-  DocumentProto person = DocumentBuilder()
-                             .SetKey("pkg$db/namespace", "person")
-                             .SetSchema("Person")
-                             .AddStringProperty("Name", "Alice")
-                             .Build();
-
-  // - "sender" (joinable property id 2) has
-  //   DELETE_PROPAGATION_TYPE_PROPAGATE_FROM.
-  // - "receiver" (joinable property id 0) has DELETE_PROPAGATION_TYPE_NONE.
-  DocumentProto message =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "message")
-          .SetSchema("Message")
-          .AddStringProperty("content", "test content")
-          .AddStringProperty("sender", "pkg$db/namespace#person")
-          .AddStringProperty("receiver", "pkg$db/namespace#person")
-          .Build();
-
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person_doc_id,
-                             this->PutAndIndexDocument(person));
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message_doc_id,
-                             this->PutAndIndexDocument(message));
-
-  // Sanity check.
-  ASSERT_THAT(
-      this->qualified_id_join_index_->Get(person_doc_id),
-      IsOkAndHolds(ElementsAre(
-          DocumentJoinIdPair(message_doc_id, /*joinable_property_id=*/0),
-          DocumentJoinIdPair(message_doc_id, /*joinable_property_id=*/2))));
-
-  JoinProcessor join_processor(
-      this->doc_store_.get(), this->schema_store_.get(),
-      this->qualified_id_join_index_.get(),
-      /*current_time_ms=*/this->fake_clock_.GetSystemTimeMilliseconds());
-
-  // Deleting the parent document should propagate the delete to its child
-  // document when there is at least one property with
-  // DELETE_PROPAGATION_TYPE_PROPAGATE_FROM.
-  EXPECT_THAT(
-      join_processor.GetPropagatedChildDocumentsToDelete({person_doc_id}),
-      IsOkAndHolds(UnorderedElementsAre(message_doc_id)));
-}
-
-TYPED_TEST(JoinProcessorTest,
-           GetPropagatedChildDocumentsToDelete_propagateToMultipleChildren) {
-  if (!std::is_same_v<typename TestFixture::JOIN_INDEX_TYPE,
-                      QualifiedIdJoinIndexImplV3>) {
-    GTEST_SKIP() << "Skipping test for non v3 QualifiedIdJoinIndex";
-  }
-
-  DocumentProto person = DocumentBuilder()
-                             .SetKey("pkg$db/namespace", "person")
-                             .SetSchema("Person")
-                             .AddStringProperty("Name", "Alice")
-                             .Build();
-
-  // "sender" (joinable property id 2) has
-  // DELETE_PROPAGATION_TYPE_PROPAGATE_FROM.
-  DocumentProto message1 =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "message1")
-          .SetSchema("Message")
-          .AddStringProperty("content", "test content")
-          .AddStringProperty("sender", "pkg$db/namespace#person")
-          .Build();
-  DocumentProto message2 =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "message2")
-          .SetSchema("Message")
-          .AddStringProperty("content", "test content")
-          .AddStringProperty("sender", "pkg$db/namespace#person")
-          .Build();
-
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person_doc_id,
-                             this->PutAndIndexDocument(person));
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message1_doc_id,
-                             this->PutAndIndexDocument(message1));
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message2_doc_id,
-                             this->PutAndIndexDocument(message2));
-
-  // Sanity check.
-  ASSERT_THAT(
-      this->qualified_id_join_index_->Get(person_doc_id),
-      IsOkAndHolds(ElementsAre(
-          DocumentJoinIdPair(message1_doc_id, /*joinable_property_id=*/2),
-          DocumentJoinIdPair(message2_doc_id, /*joinable_property_id=*/2))));
-
-  JoinProcessor join_processor(
-      this->doc_store_.get(), this->schema_store_.get(),
-      this->qualified_id_join_index_.get(),
-      /*current_time_ms=*/this->fake_clock_.GetSystemTimeMilliseconds());
-
-  // Deleting the parent document should propagate the delete to all of its
-  // child documents via the joinable property with
-  // DELETE_PROPAGATION_TYPE_PROPAGATE_FROM.
-  EXPECT_THAT(
-      join_processor.GetPropagatedChildDocumentsToDelete({person_doc_id}),
-      IsOkAndHolds(UnorderedElementsAre(message1_doc_id, message2_doc_id)));
-}
-
-TYPED_TEST(
-    JoinProcessorTest,
-    GetPropagatedChildDocumentsToDelete_propagateFromMultipleProperties) {
-  if (!std::is_same_v<typename TestFixture::JOIN_INDEX_TYPE,
-                      QualifiedIdJoinIndexImplV3>) {
-    GTEST_SKIP() << "Skipping test for non v3 QualifiedIdJoinIndex";
-  }
-
-  DocumentProto person1 = DocumentBuilder()
-                              .SetKey("pkg$db/namespace", "person1")
-                              .SetSchema("Person")
-                              .AddStringProperty("Name", "Alice")
-                              .Build();
-  DocumentProto person2 = DocumentBuilder()
-                              .SetKey("pkg$db/namespace", "person2")
-                              .SetSchema("Person")
-                              .AddStringProperty("Name", "Bob")
-                              .Build();
-
-  // "sender" (joinable property id 2) and "reporter" (joinable property id 1)
-  // have DELETE_PROPAGATION_TYPE_PROPAGATE_FROM.
-  DocumentProto message =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "message")
-          .SetSchema("Message")
-          .AddStringProperty("content", "test content")
-          .AddStringProperty("sender", "pkg$db/namespace#person1")
-          .AddStringProperty("reporter", "pkg$db/namespace#person2")
-          .Build();
-
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person1_doc_id,
-                             this->PutAndIndexDocument(person1));
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person2_doc_id,
-                             this->PutAndIndexDocument(person2));
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message_doc_id,
-                             this->PutAndIndexDocument(message));
-
-  // Sanity check.
-  ASSERT_THAT(this->qualified_id_join_index_->Get(person1_doc_id),
-              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
-                  message_doc_id, /*joinable_property_id=*/2))));
-  ASSERT_THAT(this->qualified_id_join_index_->Get(person2_doc_id),
-              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
-                  message_doc_id, /*joinable_property_id=*/1))));
-
-  JoinProcessor join_processor(
-      this->doc_store_.get(), this->schema_store_.get(),
-      this->qualified_id_join_index_.get(),
-      /*current_time_ms=*/this->fake_clock_.GetSystemTimeMilliseconds());
-
-  // message document should be propagated to be deleted from both person1 and
-  // person2 via "sender" and "reporter" properties respectively.
-  EXPECT_THAT(join_processor.GetPropagatedChildDocumentsToDelete(
-                  {person1_doc_id, person2_doc_id}),
-              IsOkAndHolds(UnorderedElementsAre(message_doc_id)));
-}
-
-TYPED_TEST(JoinProcessorTest,
-           GetPropagatedChildDocumentsToDelete_propagateToGrandChildren) {
-  if (!std::is_same_v<typename TestFixture::JOIN_INDEX_TYPE,
-                      QualifiedIdJoinIndexImplV3>) {
-    GTEST_SKIP() << "Skipping test for non v3 QualifiedIdJoinIndex";
-  }
-
-  // Create the following relations:
-  //
-  //                         ("object") - label1
-  //                        /
-  //             message1 <-
-  //            /           \
-  //      ("sender")         ("softLink") - label2
-  //          /
-  // person <-
-  //          \
-  //      ("receiver")       ("object") - label3
-  //            \           /
-  //             message2 <-
-  //                        \
-  //                         ("softLink") - label4
-  //
-  // Note: "sender" and "object" have DELETE_PROPAGATION_TYPE_PROPAGATE_FROM,
-  //       while "receiver" and "softLink" have DELETE_PROPAGATION_TYPE_NONE.
-  DocumentProto person = DocumentBuilder()
-                             .SetKey("pkg$db/namespace", "person")
-                             .SetSchema("Person")
-                             .AddStringProperty("Name", "Alice")
-                             .Build();
-
-  DocumentProto message1 =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "message1")
-          .SetSchema("Message")
-          .AddStringProperty("content", "test content")
-          .AddStringProperty("sender", "pkg$db/namespace#person")
-          .Build();
-  DocumentProto message2 =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "message2")
-          .SetSchema("Message")
-          .AddStringProperty("content", "test content")
-          .AddStringProperty("receiver", "pkg$db/namespace#person")
-          .Build();
-
-  DocumentProto label1 =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "label1")
-          .SetSchema("Label")
-          .AddStringProperty("text", " label1")
-          .AddStringProperty("object", "pkg$db/namespace#message1")
-          .Build();
-  DocumentProto label2 =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "label2")
-          .SetSchema("Label")
-          .AddStringProperty("text", " label2")
-          .AddStringProperty("softLink", "pkg$db/namespace#message1")
-          .Build();
-  DocumentProto label3 =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "label3")
-          .SetSchema("Label")
-          .AddStringProperty("text", " label3")
-          .AddStringProperty("object", "pkg$db/namespace#message2")
-          .Build();
-  DocumentProto label4 =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "label4")
-          .SetSchema("Label")
-          .AddStringProperty("text", " label4")
-          .AddStringProperty("softLink", "pkg$db/namespace#message2")
-          .Build();
-
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person_doc_id,
-                             this->PutAndIndexDocument(person));
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message1_doc_id,
-                             this->PutAndIndexDocument(message1));
-  ICING_ASSERT_OK(this->PutAndIndexDocument(message2));
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label1_doc_id,
-                             this->PutAndIndexDocument(label1));
-  ICING_ASSERT_OK(this->PutAndIndexDocument(label2));
-  ICING_ASSERT_OK(this->PutAndIndexDocument(label3));
-  ICING_ASSERT_OK(this->PutAndIndexDocument(label4));
-
-  JoinProcessor join_processor(
-      this->doc_store_.get(), this->schema_store_.get(),
-      this->qualified_id_join_index_.get(),
-      /*current_time_ms=*/this->fake_clock_.GetSystemTimeMilliseconds());
-
-  // - For children with type "Message", only message1 should be propagated to
-  //   be deleted.
-  // - For grand children with type "Label", only label1 should be propagated to
-  //   be deleted.
-  EXPECT_THAT(
-      join_processor.GetPropagatedChildDocumentsToDelete({person_doc_id}),
-      IsOkAndHolds(UnorderedElementsAre(message1_doc_id, label1_doc_id)));
-}
-
-TYPED_TEST(JoinProcessorTest,
-           GetPropagatedChildDocumentsToDelete_cycleReference) {
-  if (!std::is_same_v<typename TestFixture::JOIN_INDEX_TYPE,
-                      QualifiedIdJoinIndexImplV3>) {
-    GTEST_SKIP() << "Skipping test for non v3 QualifiedIdJoinIndex";
-  }
-
-  // Create the following relations:
-  //
-  // label1 <- label2 <- label3
-  //   |                   ^
-  //   |                   |
-  //   +-------------------+
-  DocumentProto label1 =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "label1")
-          .SetSchema("Label")
-          .AddStringProperty("text", " label1")
-          .AddStringProperty("object", "pkg$db/namespace#label3")
-          .Build();
-  DocumentProto label2 =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "label2")
-          .SetSchema("Label")
-          .AddStringProperty("text", " label2")
-          .AddStringProperty("object", "pkg$db/namespace#label1")
-          .Build();
-  DocumentProto label3 =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "label3")
-          .SetSchema("Label")
-          .AddStringProperty("text", " label3")
-          .AddStringProperty("object", "pkg$db/namespace#label2")
-          .Build();
-
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label1_doc_id_old,
-                             this->PutAndIndexDocument(label1));
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label2_doc_id,
-                             this->PutAndIndexDocument(label2));
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label3_doc_id,
-                             this->PutAndIndexDocument(label3));
-  // Put label1 again, due to the requirement of the join index: parent document
-  // must be present before the child document. This will make the relation data
-  // "label1 -> label3" present in the join index.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label1_doc_id_new,
-                             this->PutAndIndexDocument(label1));
-
-  // Sanity check for migration: put label1 2nd time.
-  ASSERT_THAT(label1_doc_id_new, Ne(label1_doc_id_old));
-  // Old label1's doc id should get children = [].
-  ASSERT_THAT(this->qualified_id_join_index_->Get(label1_doc_id_old),
-              IsOkAndHolds(IsEmpty()));
-  // New label1's doc id should get children = [label2_doc_id].
-  ASSERT_THAT(this->qualified_id_join_index_->Get(label1_doc_id_new),
-              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
-                  label2_doc_id, /*joinable_property_id=*/0))));
-  // label2 should get children = [label3_doc_id].
-  ASSERT_THAT(this->qualified_id_join_index_->Get(label2_doc_id),
-              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
-                  label3_doc_id, /*joinable_property_id=*/0))));
-  // label3 should get children = [label1_doc_id_new]. label1_doc_id_old will
-  // not be returned because when putting label1 for the 1st time, label3 was
-  // not present yet, and label1_doc_id_old will not be added to label3's
-  // children list.
-  ASSERT_THAT(this->qualified_id_join_index_->Get(label3_doc_id),
-              IsOkAndHolds(
-                  ElementsAre(DocumentJoinIdPair(label1_doc_id_new,
-                                                 /*joinable_property_id=*/0))));
-
-  JoinProcessor join_processor(
-      this->doc_store_.get(), this->schema_store_.get(),
-      this->qualified_id_join_index_.get(),
-      /*current_time_ms=*/this->fake_clock_.GetSystemTimeMilliseconds());
-
-  // Call GetPropagatedChildDocumentsToDelete with {label1_doc_id_new}:
-  // - Propagate to label2_doc_id from label1_doc_id_new.
-  // - Propagate to label3_doc_id from label2_doc_id.
-  // - When trying to propage label3_doc_id to its children =
-  //   [label1_doc_id_new]:
-  //   - label1_doc_id_new is already deleted, so it should not be propagated
-  //     again.
-  //   - There should be no infinite propagation loop.
-  EXPECT_THAT(
-      join_processor.GetPropagatedChildDocumentsToDelete({label1_doc_id_new}),
-      IsOkAndHolds(UnorderedElementsAre(label2_doc_id, label3_doc_id)));
-
-  // Call GetPropagatedChildDocumentsToDelete with {label2_doc_id}.
-  EXPECT_THAT(
-      join_processor.GetPropagatedChildDocumentsToDelete({label2_doc_id}),
-      IsOkAndHolds(UnorderedElementsAre(label1_doc_id_new, label3_doc_id)));
-
-  // Call GetPropagatedChildDocumentsToDelete with {label3_doc_id}.
-  EXPECT_THAT(
-      join_processor.GetPropagatedChildDocumentsToDelete({label3_doc_id}),
-      IsOkAndHolds(UnorderedElementsAre(label1_doc_id_new, label2_doc_id)));
-}
-
-TYPED_TEST(JoinProcessorTest,
-           GetPropagatedChildDocumentsToDelete_selfCycleReference) {
-  if (!std::is_same_v<typename TestFixture::JOIN_INDEX_TYPE,
-                      QualifiedIdJoinIndexImplV3>) {
-    GTEST_SKIP() << "Skipping test for non v3 QualifiedIdJoinIndex";
-  }
-
-  DocumentProto label =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "label")
-          .SetSchema("Label")
-          .AddStringProperty("text", " label")
-          .AddStringProperty("object", "pkg$db/namespace#label")
-          .Build();
-
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label_doc_id,
-                             this->PutAndIndexDocument(label));
-
-  // Sanity check.
-  ASSERT_THAT(this->qualified_id_join_index_->Get(label_doc_id),
-              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
-                  label_doc_id, /*joinable_property_id=*/0))));
-
-  JoinProcessor join_processor(
-      this->doc_store_.get(), this->schema_store_.get(),
-      this->qualified_id_join_index_.get(),
-      /*current_time_ms=*/this->fake_clock_.GetSystemTimeMilliseconds());
-
-  // Call GetPropagatedChildDocumentsToDelete with {label_doc_id}: should get
-  // nothing since label_doc_id is already in the deleted set. Also there
-  // should be no infinite propagation loop.
-  EXPECT_THAT(
-      join_processor.GetPropagatedChildDocumentsToDelete({label_doc_id}),
-      IsOkAndHolds(IsEmpty()));
-}
-
-TYPED_TEST(
-    JoinProcessorTest,
-    GetPropagatedChildDocumentsToDelete_shouldPropagateToExpiredDocuments) {
-  if (!std::is_same_v<typename TestFixture::JOIN_INDEX_TYPE,
-                      QualifiedIdJoinIndexImplV3>) {
-    GTEST_SKIP() << "Skipping test for non v3 QualifiedIdJoinIndex";
-  }
-
-  constexpr int64_t kCreationTimestampMs = 1000;
-  constexpr int64_t kShortTtlMs = 1000;
-  constexpr int64_t kLongTtlMs = 10000;
-
-  // Create the following relations:
-  //
-  // label1 <- label2 <- label3
-  DocumentProto label1 = DocumentBuilder()
-                             .SetKey("pkg$db/namespace", "label1")
-                             .SetSchema("Label")
-                             .AddStringProperty("text", " label1")
-                             .SetCreationTimestampMs(kCreationTimestampMs)
-                             .SetTtlMs(kLongTtlMs)
-                             .Build();
-  DocumentProto label2 =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "label2")
-          .SetSchema("Label")
-          .AddStringProperty("text", " label2")
-          .AddStringProperty("object", "pkg$db/namespace#label1")
-          .SetCreationTimestampMs(kCreationTimestampMs)
-          .SetTtlMs(kShortTtlMs)
-          .Build();
-  DocumentProto label3 =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "label3")
-          .SetSchema("Label")
-          .AddStringProperty("text", " label3")
-          .AddStringProperty("object", "pkg$db/namespace#label2")
-          .SetCreationTimestampMs(kCreationTimestampMs)
-          .SetTtlMs(kLongTtlMs)
-          .Build();
-
-  this->fake_clock_.SetSystemTimeMilliseconds(kCreationTimestampMs);
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label1_doc_id,
-                             this->PutAndIndexDocument(label1));
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label2_doc_id,
-                             this->PutAndIndexDocument(label2));
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label3_doc_id,
-                             this->PutAndIndexDocument(label3));
-
-  // Adjust the clock to expire label2.
-  int64_t current_time_ms = kCreationTimestampMs + kShortTtlMs + 100;
-  this->fake_clock_.SetSystemTimeMilliseconds(current_time_ms);
-
-  // Sanity check: label2 is expired, and label1 and label3 are not expired.
-  ASSERT_THAT(this->doc_store_->GetAliveDocumentFilterData(label1_doc_id,
-                                                           current_time_ms),
-              Ne(std::nullopt));
-  ASSERT_THAT(this->doc_store_->GetAliveDocumentFilterData(label2_doc_id,
-                                                           current_time_ms),
-              Eq(std::nullopt));
-  ASSERT_THAT(this->doc_store_->GetAliveDocumentFilterData(label3_doc_id,
-                                                           current_time_ms),
-              Ne(std::nullopt));
-  ASSERT_THAT(this->qualified_id_join_index_->Get(label1_doc_id),
-              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
-                  label2_doc_id, /*joinable_property_id=*/0))));
-  ASSERT_THAT(this->qualified_id_join_index_->Get(label2_doc_id),
-              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
-                  label3_doc_id, /*joinable_property_id=*/0))));
-  ASSERT_THAT(this->qualified_id_join_index_->Get(label3_doc_id),
-              IsOkAndHolds(IsEmpty()));
-
-  JoinProcessor join_processor(
-      this->doc_store_.get(), this->schema_store_.get(),
-      this->qualified_id_join_index_.get(),
-      /*current_time_ms=*/this->fake_clock_.GetSystemTimeMilliseconds());
-
-  // Call GetPropagatedChildDocumentsToDelete with {label1_doc_id}: should still
-  // propagate to label2_doc_id and label3_doc_id even though label2_doc_id is
-  // expired.
-  EXPECT_THAT(
-      join_processor.GetPropagatedChildDocumentsToDelete({label1_doc_id}),
-      IsOkAndHolds(UnorderedElementsAre(label2_doc_id, label3_doc_id)));
-}
-
-TYPED_TEST(
-    JoinProcessorTest,
-    GetPropagatedChildDocumentsToDelete_shouldNotPropagateToDeletedDocuments) {
-  if (!std::is_same_v<typename TestFixture::JOIN_INDEX_TYPE,
-                      QualifiedIdJoinIndexImplV3>) {
-    GTEST_SKIP() << "Skipping test for non v3 QualifiedIdJoinIndex";
-  }
-
-  // Create the following relations:
-  //
-  // label1 <- label2
-  DocumentProto label1 = DocumentBuilder()
-                             .SetKey("pkg$db/namespace", "label1")
-                             .SetSchema("Label")
-                             .AddStringProperty("text", " label1")
-                             .Build();
-  DocumentProto label2 =
-      DocumentBuilder()
-          .SetKey("pkg$db/namespace", "label2")
-          .SetSchema("Label")
-          .AddStringProperty("text", " label2")
-          .AddStringProperty("object", "pkg$db/namespace#label1")
-          .Build();
-
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label1_doc_id,
-                             this->PutAndIndexDocument(label1));
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentId label2_doc_id,
-                             this->PutAndIndexDocument(label2));
-
-  // Delete label2.
-  ICING_ASSERT_OK(this->doc_store_->Delete(
-      label2_doc_id, this->fake_clock_.GetSystemTimeMilliseconds()));
-
-  // Sanity check: label2 is deleted, but join data is still present.
-  ASSERT_THAT(this->doc_store_->GetAliveDocumentFilterData(
-                  label2_doc_id, this->fake_clock_.GetSystemTimeMilliseconds()),
-              Eq(std::nullopt));
-  ASSERT_THAT(this->qualified_id_join_index_->Get(label1_doc_id),
-              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
-                  label2_doc_id, /*joinable_property_id=*/0))));
-  ASSERT_THAT(this->qualified_id_join_index_->Get(label2_doc_id),
-              IsOkAndHolds(IsEmpty()));
-
-  JoinProcessor join_processor(
-      this->doc_store_.get(), this->schema_store_.get(),
-      this->qualified_id_join_index_.get(),
-      /*current_time_ms=*/this->fake_clock_.GetSystemTimeMilliseconds());
-
-  // Call GetPropagatedChildDocumentsToDelete with {label1_doc_id}: should not
-  // propagate to label2_doc_id.
-  EXPECT_THAT(
-      join_processor.GetPropagatedChildDocumentsToDelete({label1_doc_id}),
-      IsOkAndHolds(IsEmpty()));
-}
-
 // TODO(b/256022027): add unit tests for non-joinable property. If joinable
 //                    value type is unset, then qualifed id join should not
 //                    include the child document even if it contains a valid
diff --git a/icing/join/qualified-id-join-index-impl-v2.cc b/icing/join/qualified-id-join-index-impl-v2.cc
index fb5bd33..1d1b2ed 100644
--- a/icing/join/qualified-id-join-index-impl-v2.cc
+++ b/icing/join/qualified-id-join-index-impl-v2.cc
@@ -38,6 +38,7 @@
 #include "icing/schema/joinable-property.h"
 #include "icing/store/document-filter-data.h"
 #include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
 #include "icing/store/key-mapper.h"
 #include "icing/store/namespace-id-fingerprint.h"
 #include "icing/store/namespace-id.h"
@@ -296,6 +297,7 @@
 }
 
 libtextclassifier3::Status QualifiedIdJoinIndexImplV2::Optimize(
+    const DocumentStore* /*document_store*/,
     const std::vector<DocumentId>& document_id_old_to_new,
     const std::vector<NamespaceId>& namespace_id_old_to_new,
     DocumentId new_last_added_document_id) {
@@ -336,9 +338,9 @@
   }
 
   // Reinitialize qualified id join index.
-  if (!filesystem_.PRead(GetMetadataFilePath(working_path_).c_str(),
-                         metadata_buffer_.get(), kMetadataFileSize,
-                         /*offset=*/0)) {
+  if (filesystem_.PRead(GetMetadataFilePath(working_path_).c_str(),
+                        metadata_buffer_.get(), kMetadataFileSize,
+                        /*offset=*/0) != kMetadataFileSize) {
     return absl_ports::InternalError("Fail to read metadata file");
   }
   ICING_ASSIGN_OR_RETURN(
@@ -461,9 +463,9 @@
     bool pre_mapping_fbv) {
   // PRead metadata file.
   auto metadata_buffer = std::make_unique<uint8_t[]>(kMetadataFileSize);
-  if (!filesystem.PRead(GetMetadataFilePath(working_path).c_str(),
-                        metadata_buffer.get(), kMetadataFileSize,
-                        /*offset=*/0)) {
+  if (filesystem.PRead(GetMetadataFilePath(working_path).c_str(),
+                       metadata_buffer.get(), kMetadataFileSize,
+                       /*offset=*/0) != kMetadataFileSize) {
     return absl_ports::InternalError("Fail to read metadata file");
   }
 
diff --git a/icing/join/qualified-id-join-index-impl-v2.h b/icing/join/qualified-id-join-index-impl-v2.h
index 379f0b8..08b3e94 100644
--- a/icing/join/qualified-id-join-index-impl-v2.h
+++ b/icing/join/qualified-id-join-index-impl-v2.h
@@ -34,9 +34,11 @@
 #include "icing/join/posting-list-join-data-accessor.h"
 #include "icing/join/posting-list-join-data-serializer.h"
 #include "icing/join/qualified-id-join-index.h"
+#include "icing/join/qualified-id.h"
 #include "icing/schema/joinable-property.h"
 #include "icing/store/document-filter-data.h"
 #include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
 #include "icing/store/key-mapper.h"
 #include "icing/store/namespace-id-fingerprint.h"
 #include "icing/store/namespace-id.h"
@@ -158,8 +160,16 @@
   }
 
   // v3 only API. Returns UNIMPLEMENTED_ERROR.
-  libtextclassifier3::StatusOr<std::vector<DocumentJoinIdPair>> Get(
-      DocumentId parent_document_id) const override {
+  libtextclassifier3::Status Put(
+      const DocumentStore* document_store,
+      const DocumentJoinIdPair& child_document_join_id_pair,
+      std::vector<QualifiedId>&& parent_qualified_ids) override {
+    return absl_ports::UnimplementedError("This API is not supported in V2");
+  }
+
+  // v3 only API. Returns UNIMPLEMENTED_ERROR.
+  libtextclassifier3::StatusOr<DocumentJoinIdPairArrayView>
+  GetDocumentJoinIdPairArrayView(DocumentId parent_document_id) const override {
     return absl_ports::UnimplementedError("This API is not supported in V2");
   }
 
@@ -180,7 +190,16 @@
     return libtextclassifier3::Status::OK;
   }
 
+  // No-op since v2 stores parent information in (namespace_id,
+  // fingerprint(uri)) format and does not require parent migration.
+  libtextclassifier3::Status MigrateParent(
+      const QualifiedId& parent_qualified_id,
+      DocumentId new_document_id) override {
+    return libtextclassifier3::Status::OK;
+  }
+
   libtextclassifier3::Status Optimize(
+      const DocumentStore* document_store,
       const std::vector<DocumentId>& document_id_old_to_new,
       const std::vector<NamespaceId>& namespace_id_old_to_new,
       DocumentId new_last_added_document_id) override;
diff --git a/icing/join/qualified-id-join-index-impl-v2_test.cc b/icing/join/qualified-id-join-index-impl-v2_test.cc
index 01c972e..6e89a26 100644
--- a/icing/join/qualified-id-join-index-impl-v2_test.cc
+++ b/icing/join/qualified-id-join-index-impl-v2_test.cc
@@ -131,7 +131,7 @@
       filesystem_.PRead(metadata_file_path.c_str(), metadata_buffer.get(),
                         QualifiedIdJoinIndexImplV2::kMetadataFileSize,
                         /*offset=*/0),
-      IsTrue());
+      Eq(QualifiedIdJoinIndexImplV2::kMetadataFileSize));
 
   // Check info section
   const Info* info = reinterpret_cast<const Info*>(
@@ -372,7 +372,7 @@
     ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), metadata_buffer.get(),
                                   QualifiedIdJoinIndexImplV2::kMetadataFileSize,
                                   /*offset=*/0),
-                IsTrue());
+                Eq(QualifiedIdJoinIndexImplV2::kMetadataFileSize));
 
     // Manually change magic and update checksum
     Crcs* crcs = reinterpret_cast<Crcs*>(
@@ -425,7 +425,7 @@
     ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), metadata_buffer.get(),
                                   QualifiedIdJoinIndexImplV2::kMetadataFileSize,
                                   /*offset=*/0),
-                IsTrue());
+                Eq(QualifiedIdJoinIndexImplV2::kMetadataFileSize));
 
     // Manually corrupt all_crc
     Crcs* crcs = reinterpret_cast<Crcs*>(
@@ -474,7 +474,7 @@
     ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), metadata_buffer.get(),
                                   QualifiedIdJoinIndexImplV2::kMetadataFileSize,
                                   /*offset=*/0),
-                IsTrue());
+                Eq(QualifiedIdJoinIndexImplV2::kMetadataFileSize));
 
     // Modify info, but don't update the checksum. This would be similar to
     // corruption of info.
@@ -850,9 +850,10 @@
   namespace_id_old_to_new[4] = 0;
 
   DocumentId new_last_added_document_id = 2;
-  EXPECT_THAT(index->Optimize(document_id_old_to_new, namespace_id_old_to_new,
-                              new_last_added_document_id),
-              IsOk());
+  EXPECT_THAT(
+      index->Optimize(/*document_store=*/nullptr, document_id_old_to_new,
+                      namespace_id_old_to_new, new_last_added_document_id),
+      IsOk());
   EXPECT_THAT(index, Pointee(SizeIs(3)));
   EXPECT_THAT(index->last_added_document_id(), Eq(new_last_added_document_id));
 
@@ -961,9 +962,10 @@
   std::vector<NamespaceId> namespace_id_old_to_new = {0, 1};
 
   DocumentId new_last_added_document_id = 2;
-  EXPECT_THAT(index->Optimize(document_id_old_to_new, namespace_id_old_to_new,
-                              new_last_added_document_id),
-              IsOk());
+  EXPECT_THAT(
+      index->Optimize(/*document_store=*/nullptr, document_id_old_to_new,
+                      namespace_id_old_to_new, new_last_added_document_id),
+      IsOk());
   EXPECT_THAT(index, Pointee(SizeIs(4)));
   EXPECT_THAT(index->last_added_document_id(), Eq(new_last_added_document_id));
 
@@ -1033,7 +1035,8 @@
   std::vector<NamespaceId> namespace_id_old_to_new = {0, 1};
 
   EXPECT_THAT(
-      index->Optimize(document_id_old_to_new, namespace_id_old_to_new,
+      index->Optimize(/*document_store=*/nullptr, document_id_old_to_new,
+                      namespace_id_old_to_new,
                       /*new_last_added_document_id=*/kInvalidDocumentId),
       StatusIs(libtextclassifier3::StatusCode::INTERNAL,
                HasSubstr("Qualified id join index data document id is out of "
@@ -1089,7 +1092,8 @@
   std::vector<NamespaceId> namespace_id_old_to_new = {0, 1};
 
   EXPECT_THAT(
-      index->Optimize(document_id_old_to_new, namespace_id_old_to_new,
+      index->Optimize(/*document_store=*/nullptr, document_id_old_to_new,
+                      namespace_id_old_to_new,
                       /*new_last_added_document_id=*/kInvalidDocumentId),
       IsOk());
   EXPECT_THAT(index->last_added_document_id(), Eq(kInvalidDocumentId));
@@ -1159,9 +1163,10 @@
   namespace_id_old_to_new[5] = 0;
 
   DocumentId new_last_added_document_id = 21;
-  EXPECT_THAT(index->Optimize(document_id_old_to_new, namespace_id_old_to_new,
-                              new_last_added_document_id),
-              IsOk());
+  EXPECT_THAT(
+      index->Optimize(/*document_store=*/nullptr, document_id_old_to_new,
+                      namespace_id_old_to_new, new_last_added_document_id),
+      IsOk());
   EXPECT_THAT(index, Pointee(SizeIs(4)));
   EXPECT_THAT(index->last_added_document_id(), Eq(new_last_added_document_id));
 
@@ -1250,9 +1255,10 @@
   std::vector<NamespaceId> namespace_id_old_to_new = {2, 0, 1};
 
   DocumentId new_last_added_document_id = 1;
-  EXPECT_THAT(index->Optimize(document_id_old_to_new, namespace_id_old_to_new,
-                              new_last_added_document_id),
-              IsOk());
+  EXPECT_THAT(
+      index->Optimize(/*document_store=*/nullptr, document_id_old_to_new,
+                      namespace_id_old_to_new, new_last_added_document_id),
+      IsOk());
   EXPECT_THAT(index, Pointee(SizeIs(4)));
   EXPECT_THAT(index->last_added_document_id(), Eq(new_last_added_document_id));
 
@@ -1303,7 +1309,8 @@
   std::vector<NamespaceId> namespace_id_old_to_new = {kInvalidNamespaceId};
 
   EXPECT_THAT(
-      index->Optimize(document_id_old_to_new, namespace_id_old_to_new,
+      index->Optimize(/*document_store=*/nullptr, document_id_old_to_new,
+                      namespace_id_old_to_new,
                       /*new_last_added_document_id=*/kInvalidDocumentId),
       StatusIs(libtextclassifier3::StatusCode::INTERNAL,
                HasSubstr("Qualified id join index data ref namespace id is out "
@@ -1348,7 +1355,8 @@
   std::vector<NamespaceId> namespace_id_old_to_new(3, kInvalidNamespaceId);
 
   EXPECT_THAT(
-      index->Optimize(document_id_old_to_new, namespace_id_old_to_new,
+      index->Optimize(/*document_store=*/nullptr, document_id_old_to_new,
+                      namespace_id_old_to_new,
                       /*new_last_added_document_id=*/kInvalidDocumentId),
       IsOk());
   EXPECT_THAT(index->last_added_document_id(), Eq(kInvalidDocumentId));
diff --git a/icing/join/qualified-id-join-index-impl-v3.cc b/icing/join/qualified-id-join-index-impl-v3.cc
index bff564e..7771fc7 100644
--- a/icing/join/qualified-id-join-index-impl-v3.cc
+++ b/icing/join/qualified-id-join-index-impl-v3.cc
@@ -34,8 +34,12 @@
 #include "icing/file/memory-mapped-file.h"
 #include "icing/join/document-join-id-pair.h"
 #include "icing/join/qualified-id-join-index.h"
+#include "icing/join/qualified-id.h"
 #include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
+#include "icing/store/key-mapper.h"
 #include "icing/store/namespace-id.h"
+#include "icing/store/persistent-hash-map-key-mapper.h"
 #include "icing/util/crc32.h"
 #include "icing/util/logging.h"
 #include "icing/util/math-util.h"
@@ -49,6 +53,18 @@
 
 namespace {
 
+// This is the same as the max number of entries of document_key_mapper_ in
+// DocumentStore.
+constexpr uint32_t kQualifiedIdMapperMaxNumEntries = kMaxDocumentId + 1;
+
+// - Key (QualifiedId): 22 bytes
+//   - namespace (10 bytes)
+//   - '#' (1 byte)
+//   - uri (10 bytes)
+//   - '\0' (1 byte)
+// - Value (ArrayInfo): 12 bytes
+constexpr uint32_t kQualifiedIdMapperKVByteSize = 34;
+
 std::string MakeMetadataFilePath(std::string_view working_path) {
   return absl_ports::StrCat(working_path, "/metadata");
 }
@@ -59,11 +75,28 @@
                             "/parent_document_id_to_child_array_info");
 }
 
+std::string MakeParentQualifiedIdToChildArrayInfoFilePath(
+    std::string_view working_path) {
+  return absl_ports::StrCat(working_path,
+                            "/parent_qualified_id_to_child_array_info");
+}
+
 std::string MakeChildDocumentJoinIdPairArrayFilePath(
     std::string_view working_path) {
   return absl_ports::StrCat(working_path, "/child_document_join_id_pair_array");
 }
 
+DocumentId GetDocumentId(const DocumentStore& document_store,
+                         const QualifiedId& parent_qualified_id) {
+  libtextclassifier3::StatusOr<DocumentId> parent_doc_id_or =
+      document_store.GetDocumentId(parent_qualified_id.name_space(),
+                                   parent_qualified_id.uri());
+  if (!parent_doc_id_or.ok()) {
+    return kInvalidDocumentId;
+  }
+  return parent_doc_id_or.ValueOrDie();
+}
+
 }  // namespace
 
 /* static */ libtextclassifier3::StatusOr<
@@ -76,10 +109,14 @@
   bool parent_document_id_to_child_array_info_file_exists =
       filesystem.FileExists(
           MakeParentDocumentIdToChildArrayInfoFilePath(working_path).c_str());
+  bool parent_qualified_id_to_child_array_info_file_exists =
+      filesystem.DirectoryExists(
+          MakeParentQualifiedIdToChildArrayInfoFilePath(working_path).c_str());
   bool child_document_join_id_pair_array_file_exists = filesystem.FileExists(
       MakeChildDocumentJoinIdPairArrayFilePath(working_path).c_str());
 
-  // If all files exist, initialize from existing files.
+  // If all files exist (except parent_qualified_id_to_child_array_info),
+  // initialize from existing files.
   if (metadata_file_exists &&
       parent_document_id_to_child_array_info_file_exists &&
       child_document_join_id_pair_array_file_exists) {
@@ -90,6 +127,7 @@
   // If all files don't exist, initialize new files.
   if (!metadata_file_exists &&
       !parent_document_id_to_child_array_info_file_exists &&
+      !parent_qualified_id_to_child_array_info_file_exists &&
       !child_document_join_id_pair_array_file_exists) {
     return InitializeNewFiles(filesystem, std::move(working_path),
                               feature_flags);
@@ -109,6 +147,7 @@
   }
 }
 
+// Deprecated
 libtextclassifier3::Status QualifiedIdJoinIndexImplV3::Put(
     const DocumentJoinIdPair& child_document_join_id_pair,
     std::vector<DocumentId>&& parent_document_ids) {
@@ -145,15 +184,51 @@
   return libtextclassifier3::Status::OK;
 }
 
-libtextclassifier3::StatusOr<std::vector<DocumentJoinIdPair>>
-QualifiedIdJoinIndexImplV3::Get(DocumentId parent_document_id) const {
+libtextclassifier3::Status QualifiedIdJoinIndexImplV3::Put(
+    const DocumentStore* document_store,
+    const DocumentJoinIdPair& child_document_join_id_pair,
+    std::vector<QualifiedId>&& parent_qualified_ids) {
+  if (!feature_flags_.enable_non_existent_qualified_id_join()) {
+    return absl_ports::InvalidArgumentError(
+        "Put with QualifiedId is not enabled yet!");
+  }
+  ICING_RETURN_ERROR_IF_NULL(document_store);
+
+  if (!child_document_join_id_pair.is_valid()) {
+    return absl_ports::InvalidArgumentError("Invalid child DocumentJoinIdPair");
+  }
+  if (parent_qualified_ids.empty()) {
+    return libtextclassifier3::Status::OK;
+  }
+  SetDirty();
+
+  // Sort and dedupe parent qualified ids.
+  std::sort(parent_qualified_ids.begin(), parent_qualified_ids.end());
+  auto last =
+      std::unique(parent_qualified_ids.begin(), parent_qualified_ids.end());
+  parent_qualified_ids.erase(last, parent_qualified_ids.end());
+
+  // Append child_document_join_id_pair to each parent's DocumentJoinIdPair
+  // array.
+  for (const QualifiedId& parent_qualified_id : parent_qualified_ids) {
+    ICING_RETURN_IF_ERROR(AppendChildDocumentJoinIdPairsForParent(
+        parent_qualified_id,
+        GetDocumentId(*document_store, parent_qualified_id),
+        {child_document_join_id_pair}));
+  }
+  return libtextclassifier3::Status::OK;
+}
+
+libtextclassifier3::StatusOr<QualifiedIdJoinIndex::DocumentJoinIdPairArrayView>
+QualifiedIdJoinIndexImplV3::GetDocumentJoinIdPairArrayView(
+    DocumentId parent_document_id) const {
   if (parent_document_id < 0 || parent_document_id == kInvalidDocumentId) {
     return absl_ports::InvalidArgumentError("Invalid parent document id");
   }
 
   if (parent_document_id >=
       parent_document_id_to_child_array_info_->num_elements()) {
-    return std::vector<DocumentJoinIdPair>();
+    return DocumentJoinIdPairArrayView(/*data=*/nullptr, /*len=*/0);
   }
 
   // Get the child array info for the parent.
@@ -161,7 +236,7 @@
       const ArrayInfo* array_info,
       parent_document_id_to_child_array_info_->Get(parent_document_id));
   if (!array_info->IsValid()) {
-    return std::vector<DocumentJoinIdPair>();
+    return DocumentJoinIdPairArrayView(/*data=*/nullptr, /*len=*/0);
   }
 
   // Safe check to avoid out-of-bound access. This should never happen unless
@@ -174,11 +249,11 @@
         std::to_string(child_document_join_id_pair_array_->num_elements())));
   }
 
-  // Get the DocumentJoinIdPair array and return the child DocumentJoinIdPairs.
+  // Get the DocumentJoinIdPair array ptr and return the array view.
   ICING_ASSIGN_OR_RETURN(
       const DocumentJoinIdPair* ptr,
       child_document_join_id_pair_array_->Get(array_info->index));
-  return std::vector<DocumentJoinIdPair>(ptr, ptr + array_info->used_length);
+  return DocumentJoinIdPairArrayView(ptr, array_info->used_length);
 }
 
 libtextclassifier3::Status QualifiedIdJoinIndexImplV3::MigrateParent(
@@ -205,6 +280,10 @@
     return libtextclassifier3::Status::OK;
   }
 
+  // Set dirty for the entire storage here once we make sure there are children
+  // to migrate for this parent doc.
+  SetDirty();
+
   ICING_ASSIGN_OR_RETURN(
       bool is_extended,
       ExtendParentDocumentIdToChildArrayInfoIfNecessary(new_document_id));
@@ -224,10 +303,41 @@
   return libtextclassifier3::Status::OK;
 }
 
+libtextclassifier3::Status QualifiedIdJoinIndexImplV3::MigrateParent(
+    const QualifiedId& parent_qualified_id, DocumentId new_document_id) {
+  if (!feature_flags_.enable_non_existent_qualified_id_join()) {
+    return absl_ports::InvalidArgumentError(
+        "QualifiedId entry is not enabled yet in the index!");
+  }
+
+  if (!IsDocumentIdValid(new_document_id)) {
+    return absl_ports::InvalidArgumentError(
+        "Invalid new parent document id to migrate to");
+  }
+
+  std::string key = parent_qualified_id.ToString();
+  auto array_info_or = parent_qualified_id_to_child_array_info_->Get(key);
+  if (absl_ports::IsNotFound(array_info_or.status())) {
+    // The qualified id does not exist in the index, nothing to migrate.
+    return libtextclassifier3::Status::OK;
+  }
+  ICING_RETURN_IF_ERROR(array_info_or.status());
+
+  SetDirty();
+  ICING_RETURN_IF_ERROR(
+      ExtendParentDocumentIdToChildArrayInfoIfNecessary(new_document_id));
+  ICING_RETURN_IF_ERROR(parent_document_id_to_child_array_info_->Set(
+      new_document_id, array_info_or.ValueOrDie()));
+  return libtextclassifier3::Status::OK;
+}
+
 libtextclassifier3::Status QualifiedIdJoinIndexImplV3::Optimize(
+    const DocumentStore* document_store,
     const std::vector<DocumentId>& document_id_old_to_new,
-    const std::vector<NamespaceId>& namespace_id_old_to_new,
+    const std::vector<NamespaceId>& /*namespace_id_old_to_new*/,
     DocumentId new_last_added_document_id) {
+  ICING_RETURN_ERROR_IF_NULL(document_store);
+
   std::string temp_working_path = working_path_ + "_temp";
   ICING_RETURN_IF_ERROR(
       QualifiedIdJoinIndex::Discard(filesystem_, temp_working_path));
@@ -247,8 +357,8 @@
     ICING_ASSIGN_OR_RETURN(
         std::unique_ptr<QualifiedIdJoinIndexImplV3> new_index,
         Create(filesystem_, temp_working_path_ddir.dir(), feature_flags_));
-    ICING_RETURN_IF_ERROR(
-        TransferIndex(document_id_old_to_new, new_index.get()));
+    ICING_RETURN_IF_ERROR(TransferIndex(*document_store, document_id_old_to_new,
+                                        new_index.get()));
     new_index->set_last_added_document_id(new_last_added_document_id);
     new_index->SetDirty();
 
@@ -257,6 +367,7 @@
 
   // Destruct current index's storage instances to safely swap directories.
   child_document_join_id_pair_array_.reset();
+  parent_qualified_id_to_child_array_info_.reset();
   parent_document_id_to_child_array_info_.reset();
   metadata_mmapped_file_.reset();
   if (!filesystem_.SwapFiles(temp_working_path_ddir.dir().c_str(),
@@ -286,6 +397,15 @@
           /*pre_mapping_mmap_size=*/0));
 
   ICING_ASSIGN_OR_RETURN(
+      parent_qualified_id_to_child_array_info_,
+      PersistentHashMapKeyMapper<ArrayInfo>::Create(
+          filesystem_,
+          MakeParentQualifiedIdToChildArrayInfoFilePath(working_path_),
+          /*pre_mapping_fbv=*/false,
+          /*max_num_entries=*/kQualifiedIdMapperMaxNumEntries,
+          /*average_kv_byte_size=*/kQualifiedIdMapperKVByteSize));
+
+  ICING_ASSIGN_OR_RETURN(
       child_document_join_id_pair_array_,
       FileBackedVector<DocumentJoinIdPair>::Create(
           filesystem_, MakeChildDocumentJoinIdPairArrayFilePath(working_path_),
@@ -317,6 +437,25 @@
           FileBackedVector<ArrayInfo>::kMaxFileSize,
           /*pre_mapping_mmap_size=*/0));
 
+  parent_qualified_id_to_child_array_info_.reset();
+  // Discard and reinitialize parent_qualified_id_to_child_array_info_.
+  std::string parent_qualified_id_to_child_array_info_file_path =
+      MakeParentQualifiedIdToChildArrayInfoFilePath(working_path_);
+  if (!filesystem_.DeleteDirectoryRecursively(
+          parent_qualified_id_to_child_array_info_file_path.c_str())) {
+    return absl_ports::InternalError(absl_ports::StrCat(
+        "Failed to clear parent qualified id to child array info: ",
+        parent_qualified_id_to_child_array_info_file_path));
+  }
+  ICING_ASSIGN_OR_RETURN(
+      parent_qualified_id_to_child_array_info_,
+      PersistentHashMapKeyMapper<ArrayInfo>::Create(
+          filesystem_,
+          MakeParentQualifiedIdToChildArrayInfoFilePath(working_path_),
+          /*pre_mapping_fbv=*/false,
+          /*max_num_entries=*/kQualifiedIdMapperMaxNumEntries,
+          /*average_kv_byte_size=*/kQualifiedIdMapperKVByteSize));
+
   child_document_join_id_pair_array_.reset();
   // Discard and reinitialize child_document_join_id_pair_array.
   std::string child_document_join_id_pair_array_file_path =
@@ -378,6 +517,17 @@
           FileBackedVector<ArrayInfo>::kMaxFileSize,
           /*pre_mapping_mmap_size=*/0));
 
+  // Initialize parent_qualified_id_to_child_array_info.
+  ICING_ASSIGN_OR_RETURN(
+      std::unique_ptr<KeyMapper<ArrayInfo>>
+          parent_qualified_id_to_child_array_info,
+      PersistentHashMapKeyMapper<ArrayInfo>::Create(
+          filesystem,
+          MakeParentQualifiedIdToChildArrayInfoFilePath(working_path),
+          /*pre_mapping_fbv=*/false,
+          /*max_num_entries=*/kQualifiedIdMapperMaxNumEntries,
+          /*average_kv_byte_size=*/kQualifiedIdMapperKVByteSize));
+
   // Initialize child_document_join_id_pair_array.
   ICING_ASSIGN_OR_RETURN(
       std::unique_ptr<FileBackedVector<DocumentJoinIdPair>>
@@ -394,6 +544,7 @@
           filesystem, std::move(working_path),
           std::make_unique<MemoryMappedFile>(std::move(metadata_mmapped_file)),
           std::move(parent_document_id_to_child_array_info),
+          std::move(parent_qualified_id_to_child_array_info),
           std::move(child_document_join_id_pair_array), feature_flags));
   // Initialize info content.
   new_join_index->info().magic = Info::kMagic;
@@ -436,6 +587,17 @@
           FileBackedVector<ArrayInfo>::kMaxFileSize,
           /*pre_mapping_mmap_size=*/0));
 
+  // Initialize parent_qualified_id_to_child_array_info.
+  ICING_ASSIGN_OR_RETURN(
+      std::unique_ptr<KeyMapper<ArrayInfo>>
+          parent_qualified_id_to_child_array_info,
+      PersistentHashMapKeyMapper<ArrayInfo>::Create(
+          filesystem,
+          MakeParentQualifiedIdToChildArrayInfoFilePath(working_path),
+          /*pre_mapping_fbv=*/false,
+          /*max_num_entries=*/kQualifiedIdMapperMaxNumEntries,
+          /*average_kv_byte_size=*/kQualifiedIdMapperKVByteSize));
+
   // Initialize child_document_join_id_pair_array. Set mmap pre-mapping size to
   // 0, but MemoryMappedFile will still mmap to the file size.
   ICING_ASSIGN_OR_RETURN(
@@ -453,6 +615,7 @@
           filesystem, std::move(working_path),
           std::make_unique<MemoryMappedFile>(std::move(metadata_mmapped_file)),
           std::move(parent_document_id_to_child_array_info),
+          std::move(parent_qualified_id_to_child_array_info),
           std::move(child_document_join_id_pair_array), feature_flags));
 
   // Initialize existing PersistentStorage. Checksums will be validated.
@@ -460,12 +623,19 @@
 
   // Validate magic.
   if (join_index->info().magic != Info::kMagic) {
-    return absl_ports::FailedPreconditionError("Incorrect magic value");
+    ICING_LOG(ERROR) << "Invalid header magic for QualifiedIdJoinIndexImplV3 "
+                     << join_index->working_path_
+                     << ". Expected: " << Info::kMagic
+                     << ", actual: " << join_index->info().magic;
+    return absl_ports::FailedPreconditionError(absl_ports::StrCat(
+        "Invalid header magic for QualifiedIdJoinIndexImplV3: ",
+        join_index->working_path_));
   }
 
   return join_index;
 }
 
+// Deprecated
 libtextclassifier3::Status
 QualifiedIdJoinIndexImplV3::AppendChildDocumentJoinIdPairsForParent(
     DocumentId parent_document_id,
@@ -486,9 +656,25 @@
       const ArrayInfo* array_info,
       parent_document_id_to_child_array_info_->Get(parent_document_id));
   ICING_ASSIGN_OR_RETURN(
+      ArrayInfo new_array_info,
+      AppendToChildArray(*array_info, std::move(child_document_join_id_pairs)));
+
+  // Set ArrayInfo back to parent_document_id_to_child_array_info_.
+  ICING_RETURN_IF_ERROR(parent_document_id_to_child_array_info_->Set(
+      parent_document_id, new_array_info));
+
+  return libtextclassifier3::Status::OK;
+}
+
+libtextclassifier3::StatusOr<QualifiedIdJoinIndexImplV3::ArrayInfo>
+QualifiedIdJoinIndexImplV3::AppendToChildArray(
+    const ArrayInfo& current_array_info,
+    std::vector<DocumentJoinIdPair>&& child_document_join_id_pairs) {
+  ICING_ASSIGN_OR_RETURN(
       GetMutableAndExtendResult result,
       GetMutableAndExtendChildDocumentJoinIdPairArrayIfNecessary(
-          *array_info, /*num_to_add=*/child_document_join_id_pairs.size()));
+          current_array_info,
+          /*num_to_add=*/child_document_join_id_pairs.size()));
   // - [0, result.array_info.used_length) contain valid elements.
   // - We will write new elements starting from index
   //   result.array_info.used_length, and update the used_length.
@@ -497,13 +683,45 @@
                               /*len=*/child_document_join_id_pairs.size());
   result.array_info.used_length += child_document_join_id_pairs.size();
 
-  // Set ArrayInfo back to parent_document_id_to_child_array_info_.
-  ICING_RETURN_IF_ERROR(parent_document_id_to_child_array_info_->Set(
-      parent_document_id, result.array_info));
-
   // Update header.
   info().num_data += child_document_join_id_pairs.size();
 
+  return result.array_info;
+}
+
+libtextclassifier3::Status
+QualifiedIdJoinIndexImplV3::AppendChildDocumentJoinIdPairsForParent(
+    std::string_view parent_qualified_id_str, DocumentId parent_document_id,
+    std::vector<DocumentJoinIdPair>&& child_document_join_id_pairs) {
+  if (child_document_join_id_pairs.empty()) {
+    return libtextclassifier3::Status::OK;
+  }
+
+  auto array_info_or =
+      parent_qualified_id_to_child_array_info_->Get(parent_qualified_id_str);
+  ArrayInfo array_info = kInvalidArrayInfo;
+  if (array_info_or.ok()) {
+    array_info = std::move(array_info_or).ValueOrDie();
+  } else if (!absl_ports::IsNotFound(array_info_or.status())) {
+    return array_info_or.status();
+  }
+
+  ICING_ASSIGN_OR_RETURN(
+      ArrayInfo new_array_info,
+      AppendToChildArray(array_info, std::move(child_document_join_id_pairs)));
+
+  ICING_RETURN_IF_ERROR(parent_qualified_id_to_child_array_info_->Put(
+      parent_qualified_id_str, new_array_info));
+
+  // Update parent_document_id_to_child_array_info_ if the parent document has a
+  // valid document id.
+  if (IsDocumentIdValid(parent_document_id)) {
+    ICING_RETURN_IF_ERROR(
+        ExtendParentDocumentIdToChildArrayInfoIfNecessary(parent_document_id));
+    ICING_RETURN_IF_ERROR(parent_document_id_to_child_array_info_->Set(
+        parent_document_id, new_array_info));
+  }
+
   return libtextclassifier3::Status::OK;
 }
 
@@ -685,66 +903,101 @@
 }
 
 libtextclassifier3::Status QualifiedIdJoinIndexImplV3::TransferIndex(
+    const DocumentStore& document_store,
     const std::vector<DocumentId>& document_id_old_to_new,
     QualifiedIdJoinIndexImplV3* new_index) const {
-  for (DocumentId old_parent_doc_id = 0;
-       old_parent_doc_id <
-       parent_document_id_to_child_array_info_->num_elements();
-       ++old_parent_doc_id) {
-    if (old_parent_doc_id < 0 ||
-        old_parent_doc_id >= document_id_old_to_new.size()) {
-      // If it happens, then the data is corrupted. Return error and let the
-      // caller rebuild everything.
-      return absl_ports::InternalError(
-          "Qualified id join index data parent document id is out of range. "
-          "The index may have been corrupted.");
-    }
-
-    if (document_id_old_to_new[old_parent_doc_id] == kInvalidDocumentId) {
-      // Skip if the old parent document id is invalid after optimization.
-      continue;
-    }
-
-    ICING_ASSIGN_OR_RETURN(
-        const ArrayInfo* array_info,
-        parent_document_id_to_child_array_info_->Get(old_parent_doc_id));
-    if (!array_info->IsValid()) {
-      continue;
-    }
-    ICING_ASSIGN_OR_RETURN(
-        const DocumentJoinIdPair* ptr,
-        child_document_join_id_pair_array_->Get(array_info->index));
-
-    // Get all child DocumentJoinIdPairs and assign new child document ids.
-    std::vector<DocumentJoinIdPair> new_child_doc_join_id_pairs;
-    new_child_doc_join_id_pairs.reserve(array_info->length);
-    for (int i = 0; i < array_info->used_length; ++i) {
-      DocumentId old_child_doc_id = ptr[i].document_id();
-      if (old_child_doc_id < 0 ||
-          old_child_doc_id >= document_id_old_to_new.size()) {
+  // If the flag is disabled, use the old logic to transfer the index.
+  if (!feature_flags_.enable_non_existent_qualified_id_join()) {
+    for (DocumentId old_parent_doc_id = 0;
+         old_parent_doc_id <
+         parent_document_id_to_child_array_info_->num_elements();
+         ++old_parent_doc_id) {
+      if (old_parent_doc_id < 0 ||
+          old_parent_doc_id >= document_id_old_to_new.size()) {
         // If it happens, then the data is corrupted. Return error and let the
         // caller rebuild everything.
         return absl_ports::InternalError(
-            "Qualified id join index data child document id is out of range. "
+            "Qualified id join index data parent document id is out of range. "
             "The index may have been corrupted.");
       }
 
-      DocumentId new_child_doc_id = document_id_old_to_new[old_child_doc_id];
-      if (new_child_doc_id == kInvalidDocumentId) {
+      if (document_id_old_to_new[old_parent_doc_id] == kInvalidDocumentId) {
+        // Skip if the old parent document id is invalid after optimization.
         continue;
       }
 
-      new_child_doc_join_id_pairs.push_back(
-          DocumentJoinIdPair(new_child_doc_id, ptr[i].joinable_property_id()));
+      ICING_ASSIGN_OR_RETURN(
+          const ArrayInfo* array_info,
+          parent_document_id_to_child_array_info_->Get(old_parent_doc_id));
+      ICING_ASSIGN_OR_RETURN(
+          std::vector<DocumentJoinIdPair> new_child_doc_join_id_pairs,
+          GetTransferredChildDocumentJoinIdPairs(*array_info,
+                                                 document_id_old_to_new));
+      ICING_RETURN_IF_ERROR(new_index->AppendChildDocumentJoinIdPairsForParent(
+          document_id_old_to_new[old_parent_doc_id],
+          std::move(new_child_doc_join_id_pairs)));
     }
+    return libtextclassifier3::Status::OK;
+  }
 
+  // The parent qualified id map is the source of truth. Iterate through it to
+  // transfer the index.
+  auto iter = parent_qualified_id_to_child_array_info_->GetIterator();
+  while (iter->Advance()) {
+    ArrayInfo array_info = iter->GetValue();
+    ICING_ASSIGN_OR_RETURN(
+        std::vector<DocumentJoinIdPair> new_child_doc_join_id_pairs,
+        GetTransferredChildDocumentJoinIdPairs(array_info,
+                                               document_id_old_to_new));
+    ICING_ASSIGN_OR_RETURN(QualifiedId parent_qualified_id,
+                           QualifiedId::Parse(iter->GetKey()));
+    // This will automatically transfer the parent document id map (for existing
+    // documents) to the new index.
     ICING_RETURN_IF_ERROR(new_index->AppendChildDocumentJoinIdPairsForParent(
-        document_id_old_to_new[old_parent_doc_id],
+        iter->GetKey(), GetDocumentId(document_store, parent_qualified_id),
         std::move(new_child_doc_join_id_pairs)));
   }
+
   return libtextclassifier3::Status::OK;
 }
 
+libtextclassifier3::StatusOr<std::vector<DocumentJoinIdPair>>
+QualifiedIdJoinIndexImplV3::GetTransferredChildDocumentJoinIdPairs(
+    const QualifiedIdJoinIndexImplV3::ArrayInfo& array_info,
+    const std::vector<DocumentId>& document_id_old_to_new) const {
+  if (!array_info.IsValid()) {
+    return std::vector<DocumentJoinIdPair>();
+  }
+
+  ICING_ASSIGN_OR_RETURN(
+      const DocumentJoinIdPair* ptr,
+      child_document_join_id_pair_array_->Get(array_info.index));
+
+  // Get all child DocumentJoinIdPairs and assign new child document ids.
+  std::vector<DocumentJoinIdPair> new_child_doc_join_id_pairs;
+  new_child_doc_join_id_pairs.reserve(array_info.used_length);
+  for (int i = 0; i < array_info.used_length; ++i) {
+    DocumentId old_child_doc_id = ptr[i].document_id();
+    if (old_child_doc_id < 0 ||
+        old_child_doc_id >= document_id_old_to_new.size()) {
+      // If it happens, then the data is corrupted. Return error and let the
+      // caller rebuild everything.
+      return absl_ports::InternalError(
+          "Qualified id join index data child document id is out of range. "
+          "The index may have been corrupted.");
+    }
+
+    DocumentId new_child_doc_id = document_id_old_to_new[old_child_doc_id];
+    if (new_child_doc_id == kInvalidDocumentId) {
+      continue;
+    }
+
+    new_child_doc_join_id_pairs.push_back(
+        DocumentJoinIdPair(new_child_doc_id, ptr[i].joinable_property_id()));
+  }
+  return new_child_doc_join_id_pairs;
+}
+
 libtextclassifier3::Status QualifiedIdJoinIndexImplV3::PersistMetadataToDisk() {
   // We can skip persisting metadata to disk only if both info and storage are
   // clean.
@@ -767,6 +1020,8 @@
 
   ICING_RETURN_IF_ERROR(
       parent_document_id_to_child_array_info_->PersistToDisk());
+  ICING_RETURN_IF_ERROR(
+      parent_qualified_id_to_child_array_info_->PersistToDisk());
   ICING_RETURN_IF_ERROR(child_document_join_id_pair_array_->PersistToDisk());
 
   is_storage_dirty_ = false;
@@ -783,10 +1038,20 @@
   ICING_ASSIGN_OR_RETURN(
       Crc32 parent_document_id_to_child_array_info_crc,
       parent_document_id_to_child_array_info_->UpdateChecksum());
+  ICING_ASSIGN_OR_RETURN(
+      Crc32 parent_qualified_id_to_child_array_info_crc,
+      parent_qualified_id_to_child_array_info_->UpdateChecksum());
+  if (!feature_flags_.enable_non_existent_qualified_id_join()) {
+    // We have still called UpdateChecksum() for this storage even if the flag
+    // is off. However, for flag guarding purposes, we set the local variable to
+    // 0 to exclude it from the overall checksum.
+    parent_qualified_id_to_child_array_info_crc = Crc32(0);
+  }
   ICING_ASSIGN_OR_RETURN(Crc32 child_document_join_id_pair_array_crc,
                          child_document_join_id_pair_array_->UpdateChecksum());
 
   return Crc32(parent_document_id_to_child_array_info_crc.Get() ^
+               parent_qualified_id_to_child_array_info_crc.Get() ^
                child_document_join_id_pair_array_crc.Get());
 }
 
@@ -808,10 +1073,17 @@
   // Get checksums for all components.
   Crc32 parent_document_id_to_child_array_info_crc =
       parent_document_id_to_child_array_info_->GetChecksum();
+  Crc32 parent_qualified_id_to_child_array_info_crc(0);
+  if (feature_flags_.enable_non_existent_qualified_id_join()) {
+    ICING_ASSIGN_OR_RETURN(
+        parent_qualified_id_to_child_array_info_crc,
+        parent_qualified_id_to_child_array_info_->GetChecksum());
+  }
   Crc32 child_document_join_id_pair_array_crc =
       child_document_join_id_pair_array_->GetChecksum();
 
   return Crc32(parent_document_id_to_child_array_info_crc.Get() ^
+               parent_qualified_id_to_child_array_info_crc.Get() ^
                child_document_join_id_pair_array_crc.Get());
 }
 
diff --git a/icing/join/qualified-id-join-index-impl-v3.h b/icing/join/qualified-id-join-index-impl-v3.h
index 30483df..63f484c 100644
--- a/icing/join/qualified-id-join-index-impl-v3.h
+++ b/icing/join/qualified-id-join-index-impl-v3.h
@@ -32,9 +32,12 @@
 #include "icing/file/persistent-storage.h"
 #include "icing/join/document-join-id-pair.h"
 #include "icing/join/qualified-id-join-index.h"
+#include "icing/join/qualified-id.h"
 #include "icing/schema/joinable-property.h"
 #include "icing/store/document-filter-data.h"
 #include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
+#include "icing/store/key-mapper.h"
 #include "icing/store/namespace-id-fingerprint.h"
 #include "icing/store/namespace-id.h"
 #include "icing/util/crc32.h"
@@ -143,8 +146,8 @@
 
   ~QualifiedIdJoinIndexImplV3() override;
 
-  // Puts new join data into the index: adds a new child document and its
-  // referenced parent documents into the join index.
+  // (Deprecated) Puts new join data into the index: adds a new child document
+  // and its referenced parent documents into the join index.
   //
   // Returns:
   //   - OK on success
@@ -154,13 +157,31 @@
       const DocumentJoinIdPair& child_document_join_id_pair,
       std::vector<DocumentId>&& parent_document_ids) override;
 
-  // Gets the list of joinable children for the given parent document id.
+  // Puts new join data into the index using qualified ids: adds a new child
+  // document and its referenced parent documents into the join index.
+  //
+  // Internally, this function will update both the qualified id map and the
+  // document id map (if the parent document has a valid document id).
   //
   // Returns:
-  //   - A list of children's DocumentJoinIdPair on success
+  //   - OK on success
+  //   - INVALID_ARGUMENT_ERROR if child_document_join_id_pair is invalid
+  //   - Any FileBackedVector or KeyMapper errors
+  libtextclassifier3::Status Put(
+      const DocumentStore* document_store,
+      const DocumentJoinIdPair& child_document_join_id_pair,
+      std::vector<QualifiedId>&& parent_qualified_ids) override;
+
+  // Gets an array view of all joinable children for the given parent document
+  // id.
+  //
+  // Returns:
+  //   - A DocumentJoinIdPairArrayView object on success. If there is no edge
+  //     for a valid node_id, then an array view with data() == nullptr and
+  //     size() == 0 will be returned
   //   - Any FileBackedVector errors
-  libtextclassifier3::StatusOr<std::vector<DocumentJoinIdPair>> Get(
-      DocumentId parent_document_id) const override;
+  libtextclassifier3::StatusOr<DocumentJoinIdPairArrayView>
+  GetDocumentJoinIdPairArrayView(DocumentId parent_document_id) const override;
 
   // Migrates existing join data for a parent document from old_document_id to
   // new_document_id.
@@ -176,6 +197,22 @@
   libtextclassifier3::Status MigrateParent(DocumentId old_document_id,
                                            DocumentId new_document_id) override;
 
+  // Migrates existing join data for a parent document from its qualified id to
+  // a new document id. This is used when a document that was previously
+  // referenced as a "missing parent" (by its qualified id) is indexed and
+  // assigned a document id.
+  //
+  // Note: The entry in the parent qualified id map will be kept as is, since
+  // the qualified id is the source of truth.
+  //
+  // Returns:
+  //   - OK on success
+  //   - INVALID_ARGUMENT_ERROR if new_document_id is invalid
+  //   - Any FileBackedVector or KeyMapper errors
+  libtextclassifier3::Status MigrateParent(
+      const QualifiedId& parent_qualified_id,
+      DocumentId new_document_id) override;
+
   // v2 only API. Returns UNIMPLEMENTED_ERROR.
   libtextclassifier3::Status Put(
       SchemaTypeId schema_type_id, JoinablePropertyId joinable_property_id,
@@ -193,6 +230,7 @@
   }
 
   libtextclassifier3::Status Optimize(
+      const DocumentStore* document_store,
       const std::vector<DocumentId>& document_id_old_to_new,
       const std::vector<NamespaceId>& namespace_id_old_to_new,
       DocumentId new_last_added_document_id) override;
@@ -235,6 +273,8 @@
       std::unique_ptr<MemoryMappedFile> metadata_mmapped_file,
       std::unique_ptr<FileBackedVector<ArrayInfo>>
           parent_document_id_to_child_array_info,
+      std::unique_ptr<KeyMapper<ArrayInfo>>
+          parent_qualified_id_to_child_array_info,
       std::unique_ptr<FileBackedVector<DocumentJoinIdPair>>
           child_document_join_id_pair_array,
       const FeatureFlags& feature_flags)
@@ -242,6 +282,8 @@
         metadata_mmapped_file_(std::move(metadata_mmapped_file)),
         parent_document_id_to_child_array_info_(
             std::move(parent_document_id_to_child_array_info)),
+        parent_qualified_id_to_child_array_info_(
+            std::move(parent_qualified_id_to_child_array_info)),
         child_document_join_id_pair_array_(
             std::move(child_document_join_id_pair_array)),
         feature_flags_(feature_flags),
@@ -259,10 +301,11 @@
                           std::string working_path,
                           const FeatureFlags& feature_flags);
 
-  // Appends a list of new child DocumentJoinIdPair to the parent's
+  // (Deprecated) Appends a list of new child DocumentJoinIdPair to the parent's
   // DocumentJoinIdPair (extensible) array. If the array is invalid or doesn't
   // have enough space for new elements, then extend it and set the new array
-  // info.
+  // info. This function then updates (and only updates) the
+  // parent_document_id_to_child_array_info_ map.
   //
   // Returns:
   //   - OK on success
@@ -273,6 +316,41 @@
       DocumentId parent_document_id,
       std::vector<DocumentJoinIdPair>&& child_document_join_id_pairs);
 
+  // Appends a list of new child DocumentJoinIdPair to the parent's (by its
+  // qualified id) DocumentJoinIdPair (extensible) array. If the array is
+  // invalid or doesn't have enough space for new elements, then extend it and
+  // set the new array info. This function then updates the
+  // parent_qualified_id_to_child_array_info_ map. If the parent document has a
+  // valid document id, it also updates the
+  // parent_document_id_to_child_array_info_ map.
+  //
+  // Returns:
+  //   - OK on success
+  //   - RESOURCE_EXHAUSTED_ERROR if the new # of elements exceed
+  //     kMaxNumChildrenPerParent
+  //   - Any FileBackedVector or KeyMapper errors
+  libtextclassifier3::Status AppendChildDocumentJoinIdPairsForParent(
+      std::string_view parent_qualified_id_str, DocumentId parent_document_id,
+      std::vector<DocumentJoinIdPair>&& child_document_join_id_pairs);
+
+  libtextclassifier3::Status AppendChildDocumentJoinIdPairsForParent(
+      const QualifiedId& parent_qualified_id, DocumentId parent_document_id,
+      std::vector<DocumentJoinIdPair>&& child_document_join_id_pairs) {
+    return AppendChildDocumentJoinIdPairsForParent(
+        parent_qualified_id.ToString(), parent_document_id,
+        std::move(child_document_join_id_pairs));
+  }
+
+  // Appends a list of new child DocumentJoinIdPair to an extensible array
+  // and returns the new ArrayInfo.
+  //
+  // Returns:
+  //   - New ArrayInfo on success
+  //   - Any FileBackedVector errors
+  libtextclassifier3::StatusOr<ArrayInfo> AppendToChildArray(
+      const ArrayInfo& current_array_info,
+      std::vector<DocumentJoinIdPair>&& child_document_join_id_pairs);
+
   // Extends the parent document id to child array info if necessary, according
   // to the new parent document id.
   //
@@ -326,9 +404,24 @@
   //   - OK on success
   //   - INTERNAL_ERROR on I/O error
   libtextclassifier3::Status TransferIndex(
+      const DocumentStore& document_store,
       const std::vector<DocumentId>& document_id_old_to_new,
       QualifiedIdJoinIndexImplV3* new_index) const;
 
+  // Gets all child DocumentJoinIdPairs according to array_info, transfers child
+  // document ids according to document_id_old_to_new, and returns a new vector
+  // of DocumentJoinIdPairs. Invalid child document ids will be skipped.
+  //
+  // Returns:
+  //   - A vector of remapped DocumentJoinIdPairs on success
+  //   - INTERNAL_ERROR if any child document id is out of range of
+  //     document_id_old_to_new
+  //   - Any FileBackedVector errors
+  libtextclassifier3::StatusOr<std::vector<DocumentJoinIdPair>>
+  GetTransferredChildDocumentJoinIdPairs(
+      const QualifiedIdJoinIndexImplV3::ArrayInfo& array_info,
+      const std::vector<DocumentId>& document_id_old_to_new) const;
+
   libtextclassifier3::Status PersistMetadataToDisk() override;
 
   libtextclassifier3::Status PersistStoragesToDisk() override;
@@ -384,6 +477,17 @@
   std::unique_ptr<FileBackedVector<ArrayInfo>>
       parent_document_id_to_child_array_info_;
 
+  // Storage for mapping a parent's qualified id to the ArrayInfo. With the
+  // feature flag `enable_non_existent_qualified_id_join` on, this is the
+  // source of truth for the join index. The ArrayInfo points to an extensible
+  // array stored in child_document_join_id_pair_array_, and the extensible
+  // array contains the parent's joinable children information.
+  //
+  // The parent_document_id_to_child_array_info_ map shares the same ArrayInfo
+  // with this mapper, for documents with valid document ids.
+  std::unique_ptr<KeyMapper<ArrayInfo>>
+      parent_qualified_id_to_child_array_info_;
+
   // Storage for DocumentJoinIdPair.
   // - It is a collection of multiple extensible arrays for parents.
   // - Each extensible array contains a list of child DocumentJoinIdPair.
diff --git a/icing/join/qualified-id-join-index-impl-v3_test.cc b/icing/join/qualified-id-join-index-impl-v3_test.cc
index 114cab7..adba00c 100644
--- a/icing/join/qualified-id-join-index-impl-v3_test.cc
+++ b/icing/join/qualified-id-join-index-impl-v3_test.cc
@@ -25,17 +25,26 @@
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "icing/absl_ports/str_cat.h"
+#include "icing/document-builder.h"
 #include "icing/feature-flags.h"
 #include "icing/file/file-backed-vector.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/memory-mapped-file.h"
 #include "icing/file/persistent-storage.h"
+#include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/join/document-join-id-pair.h"
+#include "icing/join/qualified-id-join-index.h"
+#include "icing/join/qualified-id.h"
+#include "icing/portable/gzip_stream.h"
+#include "icing/schema-builder.h"
+#include "icing/schema/schema-store.h"
 #include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
-#include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/clock.h"
 #include "icing/util/crc32.h"
+#include "icing/util/document-util.h"
 
 namespace icing {
 namespace lib {
@@ -43,15 +52,18 @@
 namespace {
 
 using ::testing::ElementsAre;
+using ::testing::ElementsAreArray;
 using ::testing::Eq;
 using ::testing::Gt;
 using ::testing::HasSubstr;
 using ::testing::IsEmpty;
 using ::testing::IsFalse;
+using ::testing::IsNull;
 using ::testing::IsTrue;
 using ::testing::Lt;
 using ::testing::Ne;
 using ::testing::Not;
+using ::testing::NotNull;
 using ::testing::Pointee;
 using ::testing::SizeIs;
 
@@ -59,36 +71,122 @@
 using Info = QualifiedIdJoinIndexImplV3::Info;
 using ArrayInfo = QualifiedIdJoinIndexImplV3::ArrayInfo;
 
-class QualifiedIdJoinIndexImplV3Test : public ::testing::Test {
+class QualifiedIdJoinIndexImplV3Test : public ::testing::TestWithParam<bool> {
  protected:
   void SetUp() override {
-    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+    feature_flags_ = std::make_unique<FeatureFlags>(
+        /*enable_circular_schema_definitions=*/true,
+        /*enable_scorable_properties=*/true,
+        /*enable_embedding_quantization=*/true,
+        /*enable_repeated_field_joins=*/true,
+        /*enable_embedding_backup_generation=*/true,
+        /*enable_schema_database=*/true,
+        /*release_backup_schema_file_if_overlay_present=*/true,
+        /*enable_strict_page_byte_size_limit=*/true,
+        /*enable_smaller_decompression_buffer_size=*/true,
+        /*enable_eigen_embedding_scoring=*/true,
+        /*enable_passing_filter_to_children=*/true,
+        /*enable_proto_log_new_header_format=*/true,
+        /*enable_embedding_iterator_v2=*/true,
+        /*enable_reusable_decompression_buffer=*/true,
+        /*enable_schema_type_id_optimization=*/true,
+        /*enable_optimize_improvements=*/true,
+        /*expired_document_purge_threshold_ms=*/0,
+        /*enable_non_existent_qualified_id_join=*/GetParam(),
+        /*enable_skip_set_schema_type_equality_check=*/true,
+        /*enable_embed_query_optimization=*/true,
+        /*enable_schema_definition_deduping=*/true);
 
     base_dir_ = GetTestTempDir() + "/icing";
+    working_path_ = base_dir_ + "/qualified_id_join_index_impl_v3";
+    document_store_dir_ = base_dir_ + "/document_store";
+    schema_store_dir_ = base_dir_ + "/schema_store";
     ASSERT_THAT(filesystem_.CreateDirectoryRecursively(base_dir_.c_str()),
                 IsTrue());
+    filesystem_.CreateDirectoryRecursively(document_store_dir_.c_str());
+    filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str());
+    ICING_ASSERT_OK_AND_ASSIGN(
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &clock_, feature_flags_.get()));
+    ICING_ASSERT_OK(schema_store_->SetSchema(
+        SchemaBuilder()
+            .AddType(SchemaTypeConfigBuilder().SetType("type"))
+            .Build(),
+        /*ignore_errors_and_delete_documents=*/false));
+    ASSERT_NO_FATAL_FAILURE(CreateDocumentStore());
+  }
 
-    working_path_ = base_dir_ + "/qualified_id_join_index_impl_v3";
+  void CreateDocumentStore() {
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::CreateResult create_result,
+        DocumentStore::Create(
+            &filesystem_, document_store_dir_, &clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
+    document_store_ = std::move(create_result.document_store);
+  }
+
+  void OptimizeDocumentStore() {
+    std::string optimized_document_store_dir =
+        base_dir_ + "/document_store_optimized";
+    ASSERT_THAT(filesystem_.CreateDirectoryRecursively(
+                    optimized_document_store_dir.c_str()),
+                IsTrue());
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::OptimizeResult optimize_result,
+        document_store_->OptimizeInto(
+            optimized_document_store_dir, /*lang_segmenter=*/nullptr,
+            /*potentially_optimizable_blob_handles=*/{}));
+    document_store_.reset();
+    ASSERT_THAT(filesystem_.SwapFiles(document_store_dir_.c_str(),
+                                      optimized_document_store_dir.c_str()),
+                IsTrue());
+    ASSERT_NO_FATAL_FAILURE(CreateDocumentStore());
+  }
+
+  void FillDocumentStore(int num_documents) {
+    for (int i = 0; i < num_documents; ++i) {
+      ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder()
+              .SetKey("ns", absl_ports::StrCat("uri", std::to_string(i)))
+              .SetSchema("type")
+              .Build())));
+    }
   }
 
   void TearDown() override {
+    document_store_.reset();
+    schema_store_.reset();
     filesystem_.DeleteDirectoryRecursively(base_dir_.c_str());
   }
 
   std::unique_ptr<FeatureFlags> feature_flags_;
+  Clock clock_;
   Filesystem filesystem_;
   std::string base_dir_;
   std::string working_path_;
+  std::string document_store_dir_;
+  std::string schema_store_dir_;
+  std::unique_ptr<SchemaStore> schema_store_;
+  std::unique_ptr<DocumentStore> document_store_;
 };
 
-TEST_F(QualifiedIdJoinIndexImplV3Test, InvalidWorkingPath) {
+TEST_P(QualifiedIdJoinIndexImplV3Test, InvalidWorkingPath) {
   EXPECT_THAT(QualifiedIdJoinIndexImplV3::Create(
                   filesystem_, "/dev/null/qualified_id_join_index_impl_v3",
                   *feature_flags_),
               StatusIs(libtextclassifier3::StatusCode::INTERNAL));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test, InitializeNewFiles) {
+TEST_P(QualifiedIdJoinIndexImplV3Test, InitializeNewFiles) {
   {
     // Create new qualified id join index
     ASSERT_FALSE(filesystem_.DirectoryExists(working_path_.c_str()));
@@ -111,7 +209,7 @@
       filesystem_.PRead(metadata_file_path.c_str(), metadata_buffer.get(),
                         QualifiedIdJoinIndexImplV3::kMetadataFileSize,
                         /*offset=*/0),
-      IsTrue());
+      Eq(QualifiedIdJoinIndexImplV3::kMetadataFileSize));
 
   // Check info section
   const Info* info = reinterpret_cast<const Info*>(
@@ -125,8 +223,10 @@
   const Crcs* crcs = reinterpret_cast<const Crcs*>(
       metadata_buffer.get() +
       QualifiedIdJoinIndexImplV3::kCrcsMetadataFileOffset);
-  // There are no data in FileBackedVectors, so storages_crc should be zero.
-  EXPECT_THAT(crcs->component_crcs.storages_crc, Eq(0));
+  if (!feature_flags_->enable_non_existent_qualified_id_join()) {
+    // There are no data in FileBackedVectors, so storages_crc should be zero.
+    EXPECT_THAT(crcs->component_crcs.storages_crc, Eq(0));
+  }
   EXPECT_THAT(crcs->component_crcs.info_crc,
               Eq(Crc32(std::string_view(reinterpret_cast<const char*>(info),
                                         sizeof(Info)))
@@ -138,7 +238,7 @@
                      .Get()));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        InitializationShouldFailIfMissingMetadataFile) {
   {
     DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
@@ -179,7 +279,7 @@
           HasSubstr("Inconsistent state of qualified id join index (v3)")));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        InitializationShouldFailIfMissingParentDocumentIdToChildArrayInfoFile) {
   {
     DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
@@ -220,7 +320,7 @@
           HasSubstr("Inconsistent state of qualified id join index (v3)")));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        InitializationShouldFailIfMissingChildDocumentJoinIdPairArrayFile) {
   {
     DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
@@ -261,7 +361,45 @@
           HasSubstr("Inconsistent state of qualified id join index (v3)")));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
+       InitializationShouldFailIfMissingQualifiedIdMapperFile) {
+  if (!feature_flags_->enable_non_existent_qualified_id_join()) {
+    return;  // This test is only relevant when the feature is enabled.
+  }
+
+  {
+    // Create new qualified id join index
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+        QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                           *feature_flags_));
+
+    // Insert some data with a qualified id.
+    ICING_ASSERT_OK(index->Put(
+        document_store_.get(),
+        DocumentJoinIdPair(/*document_id=*/100, /*joinable_property_id=*/20),
+        /*parent_qualified_ids=*/
+        std::vector<QualifiedId>{QualifiedId("namespace", "uri")}));
+    ASSERT_THAT(index, Pointee(SizeIs(1)));
+
+    ICING_ASSERT_OK(index->PersistToDisk());
+  }
+
+  // Manually delete the parent_qualified_id_to_child_array_info directory.
+  const std::string dir_path = absl_ports::StrCat(
+      working_path_, "/parent_qualified_id_to_child_array_info");
+  ASSERT_THAT(filesystem_.DeleteDirectoryRecursively(dir_path.c_str()),
+              IsTrue());
+
+  // Attempt to create the qualified id join index. This should fail because of
+  // checksum mismatch.
+  EXPECT_THAT(QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                                 *feature_flags_),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION,
+                       HasSubstr("Invalid storages crc")));
+}
+
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        InitializationShouldFailWithoutPersistToDiskOrDestruction) {
   DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
                                          /*joinable_property_id=*/20);
@@ -292,7 +430,7 @@
               StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        InitializationShouldSucceedWithUpdateChecksums) {
   DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
                                          /*joinable_property_id=*/20);
@@ -324,13 +462,13 @@
                              QualifiedIdJoinIndexImplV3::Create(
                                  filesystem_, working_path_, *feature_flags_));
   EXPECT_THAT(index2, Pointee(SizeIs(2)));
-  EXPECT_THAT(index2->Get(/*parent_document_id=*/0),
+  EXPECT_THAT(index2->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
               IsOkAndHolds(ElementsAre(child_join_id_pair1)));
-  EXPECT_THAT(index2->Get(/*parent_document_id=*/1),
+  EXPECT_THAT(index2->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/1),
               IsOkAndHolds(ElementsAre(child_join_id_pair2)));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        InitializationShouldSucceedWithPersistToDisk) {
   DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
                                          /*joinable_property_id=*/20);
@@ -360,13 +498,13 @@
                              QualifiedIdJoinIndexImplV3::Create(
                                  filesystem_, working_path_, *feature_flags_));
   EXPECT_THAT(index2, Pointee(SizeIs(2)));
-  EXPECT_THAT(index2->Get(/*parent_document_id=*/0),
+  EXPECT_THAT(index2->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
               IsOkAndHolds(ElementsAre(child_join_id_pair1)));
-  EXPECT_THAT(index2->Get(/*parent_document_id=*/1),
+  EXPECT_THAT(index2->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/1),
               IsOkAndHolds(ElementsAre(child_join_id_pair2)));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        InitializationShouldSucceedAfterDestruction) {
   DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
                                          /*joinable_property_id=*/20);
@@ -400,14 +538,14 @@
         QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
                                            *feature_flags_));
     EXPECT_THAT(index, Pointee(SizeIs(2)));
-    EXPECT_THAT(index->Get(/*parent_document_id=*/0),
+    EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
                 IsOkAndHolds(ElementsAre(child_join_id_pair1)));
-    EXPECT_THAT(index->Get(/*parent_document_id=*/1),
+    EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/1),
                 IsOkAndHolds(ElementsAre(child_join_id_pair2)));
   }
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        InitializeExistingFilesWithDifferentMagicShouldFail) {
   {
     // Create new qualified id join index
@@ -433,7 +571,7 @@
     ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), metadata_buffer.get(),
                                   QualifiedIdJoinIndexImplV3::kMetadataFileSize,
                                   /*offset=*/0),
-                IsTrue());
+                Eq(QualifiedIdJoinIndexImplV3::kMetadataFileSize));
 
     // Manually change magic and update checksum
     Crcs* crcs = reinterpret_cast<Crcs*>(
@@ -453,13 +591,15 @@
 
   // Attempt to create the qualified id join index with different magic. This
   // should fail.
-  EXPECT_THAT(QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
-                                                 *feature_flags_),
-              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION,
-                       HasSubstr("Incorrect magic value")));
+  EXPECT_THAT(
+      QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                         *feature_flags_),
+      StatusIs(
+          libtextclassifier3::StatusCode::FAILED_PRECONDITION,
+          HasSubstr("Invalid header magic for QualifiedIdJoinIndexImplV3")));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        InitializeExistingFilesWithWrongAllCrcShouldFail) {
   {
     // Create new qualified id join index
@@ -485,7 +625,7 @@
     ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), metadata_buffer.get(),
                                   QualifiedIdJoinIndexImplV3::kMetadataFileSize,
                                   /*offset=*/0),
-                IsTrue());
+                Eq(QualifiedIdJoinIndexImplV3::kMetadataFileSize));
 
     // Manually corrupt all_crc
     Crcs* crcs = reinterpret_cast<Crcs*>(
@@ -507,7 +647,7 @@
                        HasSubstr("Invalid all crc")));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        InitializeExistingFilesWithCorruptedInfoShouldFail) {
   {
     // Create new qualified id join index
@@ -533,7 +673,7 @@
     ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), metadata_buffer.get(),
                                   QualifiedIdJoinIndexImplV3::kMetadataFileSize,
                                   /*offset=*/0),
-                IsTrue());
+                Eq(QualifiedIdJoinIndexImplV3::kMetadataFileSize));
 
     // Modify info, but don't update the checksum. This would be similar to
     // corruption of info.
@@ -556,7 +696,7 @@
                        HasSubstr("Invalid info crc")));
 }
 
-TEST_F(
+TEST_P(
     QualifiedIdJoinIndexImplV3Test,
     InitializeExistingFilesWithCorruptedParentDocumentIdToChildArrayInfoShouldFail) {
   {
@@ -599,7 +739,7 @@
                        HasSubstr("Invalid storages crc")));
 }
 
-TEST_F(
+TEST_P(
     QualifiedIdJoinIndexImplV3Test,
     InitializeExistingFilesWithCorruptedChildDocumentJoinIdPairArrayShouldFail) {
   {
@@ -641,7 +781,7 @@
                        HasSubstr("Invalid storages crc")));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test, Put) {
+TEST_P(QualifiedIdJoinIndexImplV3Test, Put) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
                              QualifiedIdJoinIndexImplV3::Create(
@@ -682,20 +822,22 @@
   EXPECT_THAT(index, Pointee(SizeIs(6)));
 
   // Verify Get API.
-  EXPECT_THAT(index->Get(/*parent_document_id=*/0),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
               IsOkAndHolds(ElementsAre(child_join_id_pair4)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/1),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/1),
               IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair2,
                                        child_join_id_pair5)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/2),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/2),
               IsOkAndHolds(ElementsAre(child_join_id_pair3)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/3), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/4), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/5),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/3),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/4),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/5),
               IsOkAndHolds(ElementsAre(child_join_id_pair6)));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        Put_multipleParentsInASingleJoinableProperty) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
@@ -726,33 +868,35 @@
   EXPECT_THAT(index, Pointee(SizeIs(13)));
 
   // Verify Get API.
-  EXPECT_THAT(index->Get(/*parent_document_id=*/0),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
               IsOkAndHolds(ElementsAre(child_join_id_pair2)));
   EXPECT_THAT(
-      index->Get(/*parent_document_id=*/1),
+      index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/1),
       IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair2)));
   EXPECT_THAT(
-      index->Get(/*parent_document_id=*/2),
+      index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/2),
       IsOkAndHolds(ElementsAre(child_join_id_pair2, child_join_id_pair3)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/3),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/3),
               IsOkAndHolds(ElementsAre(child_join_id_pair2)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/4),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/4),
               IsOkAndHolds(ElementsAre(child_join_id_pair1)));
   EXPECT_THAT(
-      index->Get(/*parent_document_id=*/5),
+      index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/5),
       IsOkAndHolds(ElementsAre(child_join_id_pair2, child_join_id_pair3)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/6), IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/6),
+              IsOkAndHolds(IsEmpty()));
   EXPECT_THAT(
-      index->Get(/*parent_document_id=*/7),
+      index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/7),
       IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair3)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/8),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/8),
               IsOkAndHolds(ElementsAre(child_join_id_pair2)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/9), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/10),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/9),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/10),
               IsOkAndHolds(ElementsAre(child_join_id_pair1)));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        Put_multipleParentsInMultipleJoinableProperties) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
@@ -784,33 +928,35 @@
   EXPECT_THAT(index, Pointee(SizeIs(13)));
 
   // Verify Get API.
-  EXPECT_THAT(index->Get(/*parent_document_id=*/0),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
               IsOkAndHolds(ElementsAre(child_join_id_pair2)));
   EXPECT_THAT(
-      index->Get(/*parent_document_id=*/1),
+      index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/1),
       IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair2)));
   EXPECT_THAT(
-      index->Get(/*parent_document_id=*/2),
+      index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/2),
       IsOkAndHolds(ElementsAre(child_join_id_pair2, child_join_id_pair3)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/3),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/3),
               IsOkAndHolds(ElementsAre(child_join_id_pair2)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/4),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/4),
               IsOkAndHolds(ElementsAre(child_join_id_pair1)));
   EXPECT_THAT(
-      index->Get(/*parent_document_id=*/5),
+      index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/5),
       IsOkAndHolds(ElementsAre(child_join_id_pair2, child_join_id_pair3)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/6), IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/6),
+              IsOkAndHolds(IsEmpty()));
   EXPECT_THAT(
-      index->Get(/*parent_document_id=*/7),
+      index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/7),
       IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair3)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/8),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/8),
               IsOkAndHolds(ElementsAre(child_join_id_pair2)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/9), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/10),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/9),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/10),
               IsOkAndHolds(ElementsAre(child_join_id_pair1)));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        PutShouldResizeParentDocumentIdToChildArrayInfo) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
@@ -834,14 +980,15 @@
   // Get API should return empty result for document 0 to 9.
   for (DocumentId parent_doc_id = 0; parent_doc_id < kParentDocumentId;
        ++parent_doc_id) {
-    EXPECT_THAT(index->Get(parent_doc_id), IsOkAndHolds(IsEmpty()));
+    EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(parent_doc_id),
+                IsOkAndHolds(IsEmpty()));
   }
   // Get API should return the child document for document 10.
-  EXPECT_THAT(index->Get(kParentDocumentId),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(kParentDocumentId),
               IsOkAndHolds(ElementsAre(child_join_id_pair)));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        PutShouldExtendChildDocumentJoinIdPairArray) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
@@ -885,12 +1032,13 @@
   }
   EXPECT_THAT(index, Pointee(SizeIs(102)));
 
-  EXPECT_THAT(index->Get(parent1), IsOkAndHolds(child_join_id_pairs));
-  EXPECT_THAT(index->Get(parent2),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(parent1),
+              IsOkAndHolds(ElementsAreArray(child_join_id_pairs)));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(parent2),
               IsOkAndHolds(ElementsAre(child_join_id_pair2)));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        PutLargeParentShouldHandleAddressCorrectlyForRemap) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
@@ -932,7 +1080,7 @@
   EXPECT_THAT(file_size_after, Gt(file_size_before));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        PutLargeNumberOfDataShouldHandleRemapAddressCorrectly) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
@@ -974,7 +1122,7 @@
   EXPECT_THAT(index, Pointee(SizeIs(kNumChildrenToFillFbv + 1)));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test, PutShouldSkipInvalidParentDocumentId) {
+TEST_P(QualifiedIdJoinIndexImplV3Test, PutShouldSkipInvalidParentDocumentId) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
                              QualifiedIdJoinIndexImplV3::Create(
@@ -995,15 +1143,15 @@
   EXPECT_THAT(index, Pointee(SizeIs(3)));
 
   // Verify Get API.
-  EXPECT_THAT(index->Get(/*parent_document_id=*/1),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/1),
               IsOkAndHolds(ElementsAre(child_join_id_pair)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/2),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/2),
               IsOkAndHolds(ElementsAre(child_join_id_pair)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/3),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/3),
               IsOkAndHolds(ElementsAre(child_join_id_pair)));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        PutShouldReturnInvalidArgumentErrorForInvalidChild) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
@@ -1020,24 +1168,105 @@
   EXPECT_THAT(index, Pointee(IsEmpty()));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test, GetEmptyIndex) {
+TEST_P(QualifiedIdJoinIndexImplV3Test, DocumentJoinIdPairArrayView) {
+  // Create new qualified id join index
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  // Add 2 children for parent document 0.
+  DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
+                                         /*joinable_property_id=*/20);
+  DocumentJoinIdPair child_join_id_pair2(/*document_id=*/101,
+                                         /*joinable_property_id=*/2);
+  EXPECT_THAT(index->Put(child_join_id_pair1,
+                         /*parent_document_ids=*/std::vector<DocumentId>{0}),
+              IsOk());
+  EXPECT_THAT(index->Put(child_join_id_pair2,
+                         /*parent_document_ids=*/std::vector<DocumentId>{0}),
+              IsOk());
+
+  EXPECT_THAT(index, Pointee(SizeIs(2)));
+
+  // Get array view. Test each STL style method.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      QualifiedIdJoinIndex::DocumentJoinIdPairArrayView array_view1,
+      index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0));
+  EXPECT_THAT(array_view1, Not(IsEmpty()));
+  EXPECT_THAT(array_view1.data(), NotNull());
+  EXPECT_THAT(array_view1, SizeIs(2));
+  EXPECT_THAT(array_view1.begin(), NotNull());
+  EXPECT_THAT(array_view1.end(), NotNull());
+  EXPECT_THAT(array_view1,
+              ElementsAre(child_join_id_pair1, child_join_id_pair2));
+
+  // Add 1 more child for parent document 0.
+  DocumentJoinIdPair child_join_id_pair3(/*document_id=*/102,
+                                         /*joinable_property_id=*/2);
+  EXPECT_THAT(index->Put(child_join_id_pair3,
+                         /*parent_document_ids=*/std::vector<DocumentId>{0}),
+              IsOk());
+
+  // Get array view again. Test each STL style method.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      QualifiedIdJoinIndex::DocumentJoinIdPairArrayView array_view2,
+      index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0));
+  EXPECT_THAT(array_view2, Not(IsEmpty()));
+  EXPECT_THAT(array_view2.data(), NotNull());
+  EXPECT_THAT(array_view2, SizeIs(3));
+  EXPECT_THAT(array_view2.begin(), NotNull());
+  EXPECT_THAT(array_view2.end(), NotNull());
+  EXPECT_THAT(array_view2, ElementsAre(child_join_id_pair1, child_join_id_pair2,
+                                       child_join_id_pair3));
+}
+
+TEST_P(QualifiedIdJoinIndexImplV3Test, EmptyDocumentJoinIdPairArrayView) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
                              QualifiedIdJoinIndexImplV3::Create(
                                  filesystem_, working_path_, *feature_flags_));
   EXPECT_THAT(index, Pointee(IsEmpty()));
 
-  EXPECT_THAT(index->Get(/*parent_document_id=*/0), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/1), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/2), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/3), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/4), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/5), IsOkAndHolds(IsEmpty()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      QualifiedIdJoinIndex::DocumentJoinIdPairArrayView array_view,
+      index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0));
+  EXPECT_THAT(array_view, IsEmpty());
+  EXPECT_THAT(array_view.data(), IsNull());
+  EXPECT_THAT(array_view.size(), Eq(0));
+  EXPECT_THAT(array_view.begin(), IsNull());
+  EXPECT_THAT(array_view.end(), IsNull());
+
+  // Use colon to iterate the array_view. There should be no crash and no-op.
+  for (const DocumentJoinIdPair& _ : array_view) {
+    ADD_FAILURE() << "Unexpectedly iterated the empty array_view.";
+  }
 }
 
-TEST_F(
+TEST_P(QualifiedIdJoinIndexImplV3Test,
+       GetDocumentJoinIdPairArrayView_emptyIndex) {
+  // Create new qualified id join index
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+  EXPECT_THAT(index, Pointee(IsEmpty()));
+
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/1),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/2),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/3),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/4),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/5),
+              IsOkAndHolds(IsEmpty()));
+}
+
+TEST_P(
     QualifiedIdJoinIndexImplV3Test,
-    GetShouldReturnEmptyResultWithoutAccessingArrayForNonExistingLargeParent) {
+    GetDocumentJoinIdPairArrayView_shouldReturnEmptyArrayViewForNonExistingLargeParent) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
                              QualifiedIdJoinIndexImplV3::Create(
@@ -1049,33 +1278,65 @@
       child_join_id_pair, /*parent_document_ids=*/std::vector<DocumentId>{1}));
   EXPECT_THAT(index, Pointee(SizeIs(1)));
 
-  EXPECT_THAT(index->Get(/*parent_document_id=*/0), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/1),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/1),
               IsOkAndHolds(ElementsAre(child_join_id_pair)));
 
   // Now, only parent document id 1 is in the index, so the FileBackedVector has
   // been resized to fit parent document id 1.
   // Get API for parent document id greater than 1 should return empty result
   // without accessing the FileBackedVector.
-  EXPECT_THAT(index->Get(/*parent_document_id=*/2), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/3), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(kMaxDocumentId), IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/2),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/3),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(kMaxDocumentId),
+              IsOkAndHolds(IsEmpty()));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
-       GetShouldReturnInvalidArgumentErrorForInvalidParentDocumentId) {
+TEST_P(
+    QualifiedIdJoinIndexImplV3Test,
+    GetDocumentJoinIdPairArrayView_shouldReturnEmptyArrayViewForParentWithNoChildren) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
                              QualifiedIdJoinIndexImplV3::Create(
                                  filesystem_, working_path_, *feature_flags_));
 
-  EXPECT_THAT(index->Get(/*parent_document_id=*/-1),
+  // Add a child for parent document id 2.
+  DocumentJoinIdPair child_join_id_pair(/*document_id=*/100,
+                                        /*joinable_property_id=*/20);
+  ICING_ASSERT_OK(index->Put(
+      child_join_id_pair, /*parent_document_ids=*/std::vector<DocumentId>{2}));
+  EXPECT_THAT(index, Pointee(SizeIs(1)));
+
+  // Since parent array info FBV is resized to fit parent document id 2, parent
+  // document 0 and 1 should also have array info element with invalid data
+  // index for the 2nd FBV.
+  //
+  // Getting array view for parent document 0 and 1 should return empty result
+  // when seeing invalid data index.
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/1),
+              IsOkAndHolds(IsEmpty()));
+}
+
+TEST_P(
+    QualifiedIdJoinIndexImplV3Test,
+    GetDocumentJoinIdPairArrayView_shouldReturnInvalidArgumentErrorForInvalidParentDocumentId) {
+  // Create new qualified id join index
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/-1),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
-  EXPECT_THAT(index->Get(kInvalidDocumentId),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(kInvalidDocumentId),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test, MigrateParent) {
+TEST_P(QualifiedIdJoinIndexImplV3Test, MigrateParent) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
                              QualifiedIdJoinIndexImplV3::Create(
@@ -1099,19 +1360,21 @@
   // Sanity check.
   ASSERT_THAT(index, Pointee(SizeIs(2)));
   ASSERT_THAT(
-      index->Get(parent_doc_id1),
+      index->GetDocumentJoinIdPairArrayView(parent_doc_id1),
       IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair2)));
-  ASSERT_THAT(index->Get(parent_doc_id2), IsOkAndHolds(IsEmpty()));
+  ASSERT_THAT(index->GetDocumentJoinIdPairArrayView(parent_doc_id2),
+              IsOkAndHolds(IsEmpty()));
 
   // Migrate parent document id 1 to 1024.
   EXPECT_THAT(index->MigrateParent(parent_doc_id1, parent_doc_id2), IsOk());
-  EXPECT_THAT(index->Get(parent_doc_id1), IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(parent_doc_id1),
+              IsOkAndHolds(IsEmpty()));
   EXPECT_THAT(
-      index->Get(parent_doc_id2),
+      index->GetDocumentJoinIdPairArrayView(parent_doc_id2),
       IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair2)));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test,
+TEST_P(QualifiedIdJoinIndexImplV3Test,
        MigrateParentToLargeIdShouldHandleAddressCorrectlyForRemap) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
@@ -1142,18 +1405,20 @@
   // Sanity check.
   ASSERT_THAT(index, Pointee(SizeIs(2)));
   ASSERT_THAT(
-      index->Get(parent_doc_id1),
+      index->GetDocumentJoinIdPairArrayView(parent_doc_id1),
       IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair2)));
-  ASSERT_THAT(index->Get(parent_doc_id2), IsOkAndHolds(IsEmpty()));
+  ASSERT_THAT(index->GetDocumentJoinIdPairArrayView(parent_doc_id2),
+              IsOkAndHolds(IsEmpty()));
 
   // Migrate parent document id 1 to 30000. This will
   // cause parent_document_id_to_child_array_info being extended and remap. The
   // test verifies that addresses after remap are handled correctly without
   // crashing.
   EXPECT_THAT(index->MigrateParent(parent_doc_id1, parent_doc_id2), IsOk());
-  EXPECT_THAT(index->Get(parent_doc_id1), IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(parent_doc_id1),
+              IsOkAndHolds(IsEmpty()));
   EXPECT_THAT(
-      index->Get(parent_doc_id2),
+      index->GetDocumentJoinIdPairArrayView(parent_doc_id2),
       IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair2)));
   int64_t file_size_after = filesystem_.GetFileSize(array_working_path.c_str());
   ASSERT_THAT(file_size_after, Ne(Filesystem::kBadFileSize));
@@ -1162,7 +1427,126 @@
   EXPECT_THAT(file_size_after, Gt(file_size_before));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test, SetLastAddedDocumentId) {
+TEST_P(QualifiedIdJoinIndexImplV3Test, MigrateParentShouldSetDirty) {
+  // Create new qualified id join index
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  DocumentId parent_doc_id1 = 1;
+  DocumentId parent_doc_id2 = 1024;
+
+  // Add 2 children with their parents to the index.
+  DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
+                                         /*joinable_property_id=*/0);
+  DocumentJoinIdPair child_join_id_pair2(/*document_id=*/101,
+                                         /*joinable_property_id=*/0);
+  ICING_ASSERT_OK(index->Put(
+      child_join_id_pair1,
+      /*parent_document_ids=*/std::vector<DocumentId>{parent_doc_id1}));
+  ICING_ASSERT_OK(index->Put(
+      child_join_id_pair2,
+      /*parent_document_ids=*/std::vector<DocumentId>{parent_doc_id1}));
+
+  // Sanity check.
+  ASSERT_THAT(index, Pointee(SizeIs(2)));
+  ASSERT_THAT(
+      index->GetDocumentJoinIdPairArrayView(parent_doc_id1),
+      IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair2)));
+  ASSERT_THAT(index->GetDocumentJoinIdPairArrayView(parent_doc_id2),
+              IsOkAndHolds(IsEmpty()));
+  // PersistToDisk after putting data and get the checksum. This will reset the
+  // dirty flag.
+  ICING_ASSERT_OK(index->PersistToDisk());
+  ICING_ASSERT_OK_AND_ASSIGN(Crc32 crc1, index->GetChecksum());
+
+  // Migrate parent document id 1 to 1024.
+  ICING_ASSERT_OK(index->MigrateParent(parent_doc_id1, parent_doc_id2));
+
+  // Call UpdateChecksums(). The checksum should be recomputed and be different
+  // from the previous one. This validates that MigrateParent() should set the
+  // dirty flag.
+  ICING_ASSERT_OK_AND_ASSIGN(Crc32 crc2, index->UpdateChecksums());
+  EXPECT_THAT(crc2, Ne(crc1));
+
+  // Create another qualified id join index instance with the same file. It
+  // should succeed and GetChecksum() should return the same checksum as the
+  // previous one.
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index2,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+  ICING_ASSERT_OK_AND_ASSIGN(Crc32 crc3, index2->GetChecksum());
+  EXPECT_THAT(crc3, Eq(crc2));
+}
+
+TEST_P(QualifiedIdJoinIndexImplV3Test, PutAndMigrateQualifiedId) {
+  // Create new qualified id join index
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
+                                         /*joinable_property_id=*/0);
+  DocumentJoinIdPair child_join_id_pair2(/*document_id=*/101,
+                                         /*joinable_property_id=*/0);
+  QualifiedId parent_qualified_id1("namespace", "uri1");
+  QualifiedId parent_qualified_id2("namespace", "uri2");
+  DocumentId parent_doc_id1 = 1;
+  DocumentId parent_doc_id2 = 2;
+  DocumentId parent_doc_id3 = 3;
+
+  auto put_status1 = index->Put(
+      document_store_.get(), child_join_id_pair1,
+      /*parent_qualified_ids=*/std::vector<QualifiedId>{parent_qualified_id1});
+  auto put_status2 = index->Put(
+      document_store_.get(), child_join_id_pair2,
+      /*parent_qualified_ids=*/
+      std::vector<QualifiedId>{parent_qualified_id1, parent_qualified_id2});
+
+  if (!feature_flags_->enable_non_existent_qualified_id_join()) {
+    // If the flag is not enabled, Put should fail.
+    EXPECT_THAT(put_status1,
+                StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+    EXPECT_THAT(put_status2,
+                StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+    EXPECT_THAT(index, Pointee(IsEmpty()));
+    return;
+  }
+
+  ICING_ASSERT_OK(put_status1);
+  ICING_ASSERT_OK(put_status2);
+
+  // Sanity check after Put.
+  EXPECT_THAT(index, Pointee(SizeIs(3)));
+
+  // Migrate parent_qualified_id1 to parent_doc_id1.
+  ICING_ASSERT_OK(index->MigrateParent(parent_qualified_id1, parent_doc_id1));
+
+  // Verify that children are migrated to parent_doc_id1.
+  EXPECT_THAT(
+      index->GetDocumentJoinIdPairArrayView(parent_doc_id1),
+      IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair2)));
+
+  // Migrate parent_qualified_id2 to parent_doc_id2.
+  ICING_ASSERT_OK(index->MigrateParent(parent_qualified_id2, parent_doc_id2));
+
+  // Verify that children are migrated to parent_doc_id2.
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(parent_doc_id2),
+              IsOkAndHolds(ElementsAre(child_join_id_pair2)));
+
+  // parent_qualified_id1 and parent_qualified_id2 should still exist in the
+  // qualified id mapper. Migrating them again to another parent_doc_id should
+  // succeed.
+  ICING_ASSERT_OK(index->MigrateParent(parent_qualified_id1, parent_doc_id3));
+  EXPECT_THAT(
+      index->GetDocumentJoinIdPairArrayView(parent_doc_id3),
+      IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair2)));
+  ICING_ASSERT_OK(index->MigrateParent(parent_qualified_id2, parent_doc_id3));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(parent_doc_id3),
+              IsOkAndHolds(ElementsAre(child_join_id_pair2)));
+}
+
+TEST_P(QualifiedIdJoinIndexImplV3Test, SetLastAddedDocumentId) {
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
                              QualifiedIdJoinIndexImplV3::Create(
                                  filesystem_, working_path_, *feature_flags_));
@@ -1178,7 +1562,7 @@
   EXPECT_THAT(index->last_added_document_id(), Eq(kNextDocumentId));
 }
 
-TEST_F(
+TEST_P(
     QualifiedIdJoinIndexImplV3Test,
     SetLastAddedDocumentIdShouldIgnoreNewDocumentIdNotGreaterThanTheCurrent) {
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
@@ -1196,12 +1580,15 @@
   EXPECT_THAT(index->last_added_document_id(), Eq(kDocumentId));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test, Optimize) {
+TEST_P(QualifiedIdJoinIndexImplV3Test, Optimize) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
                              QualifiedIdJoinIndexImplV3::Create(
                                  filesystem_, working_path_, *feature_flags_));
 
+  // Add documents for parent doc ids to the document store.
+  ASSERT_NO_FATAL_FAILURE(FillDocumentStore(/*num_documents=*/5));
+
   // Create 4 parent and 7 child documents (with N to N joins):
   // - Document 1: 101, 103, 104, 105, 107
   // - Document 2: 102, 103, 105
@@ -1222,27 +1609,62 @@
                                          /*joinable_property_id=*/0);
   DocumentJoinIdPair child_join_id_pair7(/*document_id=*/107,
                                          /*joinable_property_id=*/0);
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair1,
-                 /*parent_document_ids=*/std::vector<DocumentId>{1, 3}));
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair2,
-                 /*parent_document_ids=*/std::vector<DocumentId>{2}));
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair3,
-                 /*parent_document_ids=*/std::vector<DocumentId>{1, 2, 4}));
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair4,
-                 /*parent_document_ids=*/std::vector<DocumentId>{1}));
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair5,
-                 /*parent_document_ids=*/std::vector<DocumentId>{1, 2}));
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair6,
-                 /*parent_document_ids=*/std::vector<DocumentId>{3}));
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair7,
-                 /*parent_document_ids=*/std::vector<DocumentId>{1}));
+
+  if (!feature_flags_->enable_non_existent_qualified_id_join()) {
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair1,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1, 3}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair2,
+                   /*parent_document_ids=*/std::vector<DocumentId>{2}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair3,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1, 2, 4}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair4,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair5,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1, 2}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair6,
+                   /*parent_document_ids=*/std::vector<DocumentId>{3}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair7,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1}));
+  } else {
+    // With the dual-tracking implementation, we need to use the Put overload
+    // that takes qualified ids to populate the qualified id mapper, which is
+    // the source of truth during Optimize().
+    QualifiedId parent_qid1("ns", "uri1");
+    QualifiedId parent_qid2("ns", "uri2");
+    QualifiedId parent_qid3("ns", "uri3");
+    QualifiedId parent_qid4("ns", "uri4");
+    ICING_ASSERT_OK(
+        index->Put(document_store_.get(), child_join_id_pair1,
+                   /*parent_qualified_ids=*/
+                   std::vector<QualifiedId>{parent_qid1, parent_qid3}));
+    ICING_ASSERT_OK(index->Put(
+        document_store_.get(), child_join_id_pair2,
+        /*parent_qualified_ids=*/std::vector<QualifiedId>{parent_qid2}));
+    ICING_ASSERT_OK(index->Put(
+        document_store_.get(), child_join_id_pair3,
+        /*parent_qualified_ids=*/
+        std::vector<QualifiedId>{parent_qid1, parent_qid2, parent_qid4}));
+    ICING_ASSERT_OK(index->Put(
+        document_store_.get(), child_join_id_pair4,
+        /*parent_qualified_ids=*/std::vector<QualifiedId>{parent_qid1}));
+    ICING_ASSERT_OK(
+        index->Put(document_store_.get(), child_join_id_pair5,
+                   /*parent_qualified_ids=*/
+                   std::vector<QualifiedId>{parent_qid1, parent_qid2}));
+    ICING_ASSERT_OK(index->Put(
+        document_store_.get(), child_join_id_pair6,
+        /*parent_qualified_ids=*/std::vector<QualifiedId>{parent_qid3}));
+    ICING_ASSERT_OK(index->Put(
+        document_store_.get(), child_join_id_pair7,
+        /*parent_qualified_ids=*/std::vector<QualifiedId>{parent_qid1}));
+  }
 
   ASSERT_THAT(index, Pointee(SizeIs(11)));
   index->set_last_added_document_id(107);
@@ -1260,21 +1682,32 @@
   document_id_old_to_new[105] = 14;
   document_id_old_to_new[106] = 15;
 
+  // Update the document store so that Join index can get the correct document
+  // ids for parents.
+  ICING_ASSERT_OK(document_store_->Delete("ns", "uri0", /*current_time_ms=*/0));
+  ICING_ASSERT_OK(document_store_->Delete("ns", "uri3", /*current_time_ms=*/0));
+  ASSERT_NO_FATAL_FAILURE(OptimizeDocumentStore());
+
   // Note: namespace_id_old_to_new is not used in
   // QualifiedIdJoinIndexImplV3::Optimize.
   DocumentId new_last_added_document_id = 15;
-  EXPECT_THAT(
-      index->Optimize(document_id_old_to_new, /*namespace_id_old_to_new=*/{},
-                      new_last_added_document_id),
-      IsOk());
-  EXPECT_THAT(index, Pointee(SizeIs(5)));
+  EXPECT_THAT(index->Optimize(document_store_.get(), document_id_old_to_new,
+                              /*namespace_id_old_to_new=*/{},
+                              new_last_added_document_id),
+              IsOk());
+  if (!feature_flags_->enable_non_existent_qualified_id_join()) {
+    EXPECT_THAT(index, Pointee(SizeIs(5)));
+  } else {
+    // Join index will no longer drop join relations for non-existent parents.
+    EXPECT_THAT(index, Pointee(SizeIs(7)));
+  }
   EXPECT_THAT(index->last_added_document_id(), Eq(new_last_added_document_id));
 
   // Verify document 0 (originally document 1)
   // - Child docs 101, 104, 105 become 11, 13, 14.
   // - Child docs 103, 107 are deleted.
   EXPECT_THAT(
-      index->Get(/*parent_document_id=*/0),
+      index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
       IsOkAndHolds(ElementsAre(
           DocumentJoinIdPair(/*document_id=*/11, /*joinable_property_id=*/0),
           DocumentJoinIdPair(/*document_id=*/13, /*joinable_property_id=*/0),
@@ -1284,20 +1717,23 @@
   // - Child docs 102, 105 become 12, 14.
   // - Child doc 103 is deleted.
   EXPECT_THAT(
-      index->Get(/*parent_document_id=*/1),
+      index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/1),
       IsOkAndHolds(ElementsAre(
           DocumentJoinIdPair(/*document_id=*/12, /*joinable_property_id=*/0),
           DocumentJoinIdPair(/*document_id=*/14, /*joinable_property_id=*/0))));
 
   // Verify document 2 (originally document 4)
   // - Child doc 103 is deleted.
-  EXPECT_THAT(index->Get(/*parent_document_id=*/2), IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/2),
+              IsOkAndHolds(IsEmpty()));
 
   // Verify document 3 and 4:
   // - These 2 doc ids don't exist after optimize.
   // - The relations for the original document 3 and 4 should be deleted.
-  EXPECT_THAT(index->Get(/*parent_document_id=*/3), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/4), IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/3),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/4),
+              IsOkAndHolds(IsEmpty()));
 
   // Verify Put API should work normally after Optimize().
   DocumentJoinIdPair another_child_join_id_pair(/*document_id=*/16,
@@ -1308,10 +1744,15 @@
       IsOk());
   index->set_last_added_document_id(16);
 
-  EXPECT_THAT(index, Pointee(SizeIs(8)));
+  if (!feature_flags_->enable_non_existent_qualified_id_join()) {
+    EXPECT_THAT(index, Pointee(SizeIs(8)));
+  } else {
+    // Join index will no longer drop join relations for non-existent parents.
+    EXPECT_THAT(index, Pointee(SizeIs(10)));
+  }
   EXPECT_THAT(index->last_added_document_id(), Eq(16));
   EXPECT_THAT(
-      index->Get(/*parent_document_id=*/0),
+      index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
       IsOkAndHolds(ElementsAre(DocumentJoinIdPair(/*document_id=*/11,
                                                   /*joinable_property_id=*/0),
                                DocumentJoinIdPair(/*document_id=*/13,
@@ -1319,13 +1760,21 @@
                                DocumentJoinIdPair(/*document_id=*/14,
                                                   /*joinable_property_id=*/0),
                                another_child_join_id_pair)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/2),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/2),
               IsOkAndHolds(ElementsAre(another_child_join_id_pair)));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/3),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/3),
               IsOkAndHolds(ElementsAre(another_child_join_id_pair)));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test, OptimizeOutOfRangeParentDocumentId) {
+TEST_P(QualifiedIdJoinIndexImplV3Test, OptimizeOutOfRangeParentDocumentId) {
+  if (feature_flags_->enable_non_existent_qualified_id_join()) {
+    // Not applicable for the dual-tracking implementation. Optimize() will use
+    // the qualified id mapper as the source of truth, and parent document ids
+    // will be read from the document store directly using qualified ids,
+    // instead of document_id_old_to_new.
+    return;
+  }
+
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
                              QualifiedIdJoinIndexImplV3::Create(
@@ -1369,19 +1818,23 @@
   // QualifiedIdJoinIndexImplV3::Optimize.
   DocumentId new_last_added_document_id = 12;
   EXPECT_THAT(
-      index->Optimize(document_id_old_to_new, /*namespace_id_old_to_new=*/{},
+      index->Optimize(document_store_.get(), document_id_old_to_new,
+                      /*namespace_id_old_to_new=*/{},
                       new_last_added_document_id),
       StatusIs(libtextclassifier3::StatusCode::INTERNAL,
                HasSubstr("Qualified id join index data parent document id is "
                          "out of range. The index may have been corrupted.")));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test, OptimizeOutOfRangeChildDocumentId) {
+TEST_P(QualifiedIdJoinIndexImplV3Test, OptimizeOutOfRangeChildDocumentId) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
                              QualifiedIdJoinIndexImplV3::Create(
                                  filesystem_, working_path_, *feature_flags_));
 
+  // Add documents for parent doc ids to the document store.
+  ASSERT_NO_FATAL_FAILURE(FillDocumentStore(/*num_documents=*/3));
+
   // Create 2 parent and 3 child documents (with N to N joins):
   // - Document 1: 101, 106, 108
   // - Document 120: 101
@@ -1392,15 +1845,30 @@
                                          /*joinable_property_id=*/0);
   DocumentJoinIdPair child_join_id_pair3(/*document_id=*/108,
                                          /*joinable_property_id=*/0);
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair1,
-                 /*parent_document_ids=*/std::vector<DocumentId>{1, 2}));
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair2,
-                 /*parent_document_ids=*/std::vector<DocumentId>{1}));
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair3,
-                 /*parent_document_ids=*/std::vector<DocumentId>{1}));
+  if (!feature_flags_->enable_non_existent_qualified_id_join()) {
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair1,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1, 2}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair2,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair3,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1}));
+  } else {
+    QualifiedId parent_qid1("ns", "uri1");
+    QualifiedId parent_qid2("ns", "uri2");
+    ICING_ASSERT_OK(
+        index->Put(document_store_.get(), child_join_id_pair1,
+                   /*parent_qualified_ids=*/
+                   std::vector<QualifiedId>{parent_qid1, parent_qid2}));
+    ICING_ASSERT_OK(index->Put(
+        document_store_.get(), child_join_id_pair2,
+        /*parent_qualified_ids=*/std::vector<QualifiedId>{parent_qid1}));
+    ICING_ASSERT_OK(index->Put(
+        document_store_.get(), child_join_id_pair3,
+        /*parent_qualified_ids=*/std::vector<QualifiedId>{parent_qid1}));
+  }
 
   ASSERT_THAT(index, Pointee(SizeIs(4)));
   index->set_last_added_document_id(120);
@@ -1416,23 +1884,32 @@
   document_id_old_to_new[101] = 11;
   document_id_old_to_new[106] = 12;
 
+  // Update the document store so that Join index can get the correct document
+  // ids for parents.
+  ICING_ASSERT_OK(document_store_->Delete("ns", "uri0", /*current_time_ms=*/0));
+  ASSERT_NO_FATAL_FAILURE(OptimizeDocumentStore());
+
   // Note: namespace_id_old_to_new is not used in
   // QualifiedIdJoinIndexImplV3::Optimize.
   DocumentId new_last_added_document_id = 12;
   EXPECT_THAT(
-      index->Optimize(document_id_old_to_new, /*namespace_id_old_to_new=*/{},
+      index->Optimize(document_store_.get(), document_id_old_to_new,
+                      /*namespace_id_old_to_new=*/{},
                       new_last_added_document_id),
       StatusIs(libtextclassifier3::StatusCode::INTERNAL,
                HasSubstr("Qualified id join index data child document id is "
                          "out of range. The index may have been corrupted.")));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test, OptimizeDeleteAllDocuments) {
+TEST_P(QualifiedIdJoinIndexImplV3Test, OptimizeDeleteAllDocuments) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
                              QualifiedIdJoinIndexImplV3::Create(
                                  filesystem_, working_path_, *feature_flags_));
 
+  // Add documents for parent doc ids to the document store.
+  ASSERT_NO_FATAL_FAILURE(FillDocumentStore(/*num_documents=*/5));
+
   // Create 4 parent and 7 child documents (with N to N joins):
   // - Document 1: 101, 103, 104, 105, 107
   // - Document 2: 102, 103, 105
@@ -1453,27 +1930,59 @@
                                          /*joinable_property_id=*/0);
   DocumentJoinIdPair child_join_id_pair7(/*document_id=*/107,
                                          /*joinable_property_id=*/0);
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair1,
-                 /*parent_document_ids=*/std::vector<DocumentId>{1, 3}));
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair2,
-                 /*parent_document_ids=*/std::vector<DocumentId>{2}));
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair3,
-                 /*parent_document_ids=*/std::vector<DocumentId>{1, 2, 4}));
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair4,
-                 /*parent_document_ids=*/std::vector<DocumentId>{1}));
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair5,
-                 /*parent_document_ids=*/std::vector<DocumentId>{1, 2}));
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair6,
-                 /*parent_document_ids=*/std::vector<DocumentId>{3}));
-  ICING_ASSERT_OK(
-      index->Put(child_join_id_pair7,
-                 /*parent_document_ids=*/std::vector<DocumentId>{1}));
+
+  if (!feature_flags_->enable_non_existent_qualified_id_join()) {
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair1,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1, 3}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair2,
+                   /*parent_document_ids=*/std::vector<DocumentId>{2}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair3,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1, 2, 4}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair4,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair5,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1, 2}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair6,
+                   /*parent_document_ids=*/std::vector<DocumentId>{3}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair7,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1}));
+  } else {
+    QualifiedId parent_qid1("ns", "uri1");
+    QualifiedId parent_qid2("ns", "uri2");
+    QualifiedId parent_qid3("ns", "uri3");
+    QualifiedId parent_qid4("ns", "uri4");
+    ICING_ASSERT_OK(
+        index->Put(document_store_.get(), child_join_id_pair1,
+                   /*parent_qualified_ids=*/
+                   std::vector<QualifiedId>{parent_qid1, parent_qid3}));
+    ICING_ASSERT_OK(index->Put(
+        document_store_.get(), child_join_id_pair2,
+        /*parent_qualified_ids=*/std::vector<QualifiedId>{parent_qid2}));
+    ICING_ASSERT_OK(index->Put(
+        document_store_.get(), child_join_id_pair3,
+        /*parent_qualified_ids=*/
+        std::vector<QualifiedId>{parent_qid1, parent_qid2, parent_qid4}));
+    ICING_ASSERT_OK(index->Put(
+        document_store_.get(), child_join_id_pair4,
+        /*parent_qualified_ids=*/std::vector<QualifiedId>{parent_qid1}));
+    ICING_ASSERT_OK(
+        index->Put(document_store_.get(), child_join_id_pair5,
+                   /*parent_qualified_ids=*/
+                   std::vector<QualifiedId>{parent_qid1, parent_qid2}));
+    ICING_ASSERT_OK(index->Put(
+        document_store_.get(), child_join_id_pair6,
+        /*parent_qualified_ids=*/std::vector<QualifiedId>{parent_qid3}));
+    ICING_ASSERT_OK(index->Put(
+        document_store_.get(), child_join_id_pair7,
+        /*parent_qualified_ids=*/std::vector<QualifiedId>{parent_qid1}));
+  }
 
   ASSERT_THAT(index, Pointee(SizeIs(11)));
   index->set_last_added_document_id(107);
@@ -1482,22 +1991,262 @@
   // Delete all documents.
   std::vector<DocumentId> document_id_old_to_new(108, kInvalidDocumentId);
 
+  // Update the document store so that Join index can get the correct document
+  // ids for parents.
+  ICING_ASSERT_OK(document_store_->Delete("ns", "uri0", /*current_time_ms=*/0));
+  ICING_ASSERT_OK(document_store_->Delete("ns", "uri1", /*current_time_ms=*/0));
+  ICING_ASSERT_OK(document_store_->Delete("ns", "uri2", /*current_time_ms=*/0));
+  ICING_ASSERT_OK(document_store_->Delete("ns", "uri3", /*current_time_ms=*/0));
+  ICING_ASSERT_OK(document_store_->Delete("ns", "uri4", /*current_time_ms=*/0));
+  ASSERT_NO_FATAL_FAILURE(OptimizeDocumentStore());
+
   // Note: namespace_id_old_to_new is not used in
   // QualifiedIdJoinIndexImplV3::Optimize.
   DocumentId new_last_added_document_id = kInvalidDocumentId;
-  EXPECT_THAT(
-      index->Optimize(document_id_old_to_new, /*namespace_id_old_to_new=*/{},
-                      new_last_added_document_id),
-      IsOk());
+  EXPECT_THAT(index->Optimize(document_store_.get(), document_id_old_to_new,
+                              /*namespace_id_old_to_new=*/{},
+                              new_last_added_document_id),
+              IsOk());
   EXPECT_THAT(index, Pointee(IsEmpty()));
   EXPECT_THAT(index->last_added_document_id(), Eq(new_last_added_document_id));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/0), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/1), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/2), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/3), IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/1),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/2),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/3),
+              IsOkAndHolds(IsEmpty()));
 }
 
-TEST_F(QualifiedIdJoinIndexImplV3Test, Clear) {
+TEST_P(QualifiedIdJoinIndexImplV3Test, OptimizeWithMissingParents) {
+  // Create new qualified id join index
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  // Add a document for the existing parent.
+  // Doc id 0: dummy
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
+  // Doc id 1
+  ICING_ASSERT_OK(document_store_->Put(
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri1_existing")
+                                               .SetSchema("type")
+                                               .Build())));
+
+  // Add join data with both regular and missing parents.
+  DocumentId parent_doc_id1 = 1;
+  QualifiedId parent_qid1("ns", "uri1_existing");
+  QualifiedId missing_parent_qualified_id1("namespace", "uri1_missing");
+  QualifiedId missing_parent_qualified_id2("namespace", "uri2_missing");
+
+  DocumentJoinIdPair child_join_id_pair1(/*document_id=*/101,
+                                         /*joinable_property_id=*/0);
+  DocumentJoinIdPair child_join_id_pair2(/*document_id=*/102,
+                                         /*joinable_property_id=*/0);
+  DocumentJoinIdPair child_join_id_pair3(/*document_id=*/103,
+                                         /*joinable_property_id=*/0);
+
+  if (!feature_flags_->enable_non_existent_qualified_id_join()) {
+    ICING_ASSERT_OK(index->Put(
+        child_join_id_pair1,
+        /*parent_document_ids=*/std::vector<DocumentId>{parent_doc_id1}));
+  } else {
+    ICING_ASSERT_OK(index->Put(
+        document_store_.get(), child_join_id_pair1,
+        /*parent_qualified_ids=*/std::vector<QualifiedId>{parent_qid1}));
+  }
+  auto missing_parent1_put_status =
+      index->Put(document_store_.get(), child_join_id_pair2,
+                 /*parent_qualified_ids=*/
+                 std::vector<QualifiedId>{missing_parent_qualified_id1});
+  auto missing_parent2_put_status =
+      index->Put(document_store_.get(), child_join_id_pair3,
+                 /*parent_qualified_ids=*/
+                 std::vector<QualifiedId>{missing_parent_qualified_id1,
+                                          missing_parent_qualified_id2});
+
+  if (!feature_flags_->enable_non_existent_qualified_id_join()) {
+    // If the flag is not enabled, put should fail.
+    EXPECT_THAT(missing_parent1_put_status,
+                StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+    EXPECT_THAT(missing_parent2_put_status,
+                StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+    ASSERT_THAT(index, Pointee(SizeIs(1)));
+  } else {
+    ICING_ASSERT_OK(missing_parent1_put_status);
+    ICING_ASSERT_OK(missing_parent2_put_status);
+    ASSERT_THAT(index, Pointee(SizeIs(4)));
+  }
+  index->set_last_added_document_id(103);
+  ASSERT_THAT(index->last_added_document_id(), Eq(103));
+
+  // Remap doc ids. Delete child 102.
+  std::vector<DocumentId> document_id_old_to_new(104, kInvalidDocumentId);
+  document_id_old_to_new[1] = 0;     // parent_doc_id1 -> 0
+  document_id_old_to_new[101] = 11;  // child_join_id_pair1 -> 11
+  document_id_old_to_new[103] = 13;  // child_join_id_pair3 -> 13
+
+  // Update the document store so that Join index can get the correct document
+  // ids for parents.
+  ICING_ASSERT_OK(document_store_->Delete("ns", "uri0", /*current_time_ms=*/0));
+  ASSERT_NO_FATAL_FAILURE(OptimizeDocumentStore());
+
+  DocumentId new_last_added_document_id = 13;
+  ICING_ASSERT_OK(index->Optimize(document_store_.get(), document_id_old_to_new,
+                                  /*namespace_id_old_to_new=*/{},
+                                  new_last_added_document_id));
+
+  // Verify parent_doc_id1 (now 0).
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
+              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
+                  /*document_id=*/11, /*joinable_property_id=*/0))));
+
+  if (!feature_flags_->enable_non_existent_qualified_id_join()) {
+    // Nothing more to check if the flag is disabled.
+    EXPECT_THAT(index, Pointee(SizeIs(1)));
+    return;
+  }
+
+  EXPECT_THAT(index, Pointee(SizeIs(3)));  // 1 from regular, 2 from missing
+
+  // Migrate missing parents and verify.
+  DocumentId new_parent_doc_id1 = 1;
+  DocumentId new_parent_doc_id2 = 2;
+  ICING_ASSERT_OK(
+      index->MigrateParent(missing_parent_qualified_id1, new_parent_doc_id1));
+  ICING_ASSERT_OK(
+      index->MigrateParent(missing_parent_qualified_id2, new_parent_doc_id2));
+
+  // child 102 was deleted, so missing_parent1 should only have child 103 (now
+  // 13).
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(new_parent_doc_id1),
+              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
+                  /*document_id=*/13, /*joinable_property_id=*/0))));
+  // missing_parent2 had child 103 (now 13).
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(new_parent_doc_id2),
+              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
+                  /*document_id=*/13, /*joinable_property_id=*/0))));
+}
+
+TEST_P(QualifiedIdJoinIndexImplV3Test,
+       OptimizeShouldKeepJoinDataOfDeletedParents) {
+  if (!feature_flags_->enable_non_existent_qualified_id_join()) {
+    return;
+  }
+
+  // Index a parent document.
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  QualifiedId parent_qualified_id("ns", "uri0");
+  // Doc id 0
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
+  // Doc id 1
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
+
+  auto doc_id_or = document_store_->GetDocumentId("ns", "uri0");
+  ASSERT_THAT(doc_id_or, IsOk());
+  DocumentId parent_doc_id = doc_id_or.ValueOrDie();
+  ASSERT_THAT(parent_doc_id, Eq(0));
+
+  // Index a join relation.
+  DocumentJoinIdPair child_join_id_pair(/*document_id=*/100,
+                                        /*joinable_property_id=*/0);
+  ICING_ASSERT_OK(index->Put(
+      document_store_.get(), child_join_id_pair,
+      /*parent_qualified_ids=*/std::vector<QualifiedId>{parent_qualified_id}));
+
+  // Verify the join relation.
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(parent_doc_id),
+              IsOkAndHolds(ElementsAre(child_join_id_pair)));
+
+  // Delete the parent document.
+  ICING_ASSERT_OK(document_store_->Delete("ns", "uri0", /*current_time_ms=*/0));
+
+  // Run Optimize.
+  // After optimize, doc "ns", "uri1" will be doc 0.
+  std::vector<DocumentId> document_id_old_to_new(101, kInvalidDocumentId);
+  document_id_old_to_new[1] = 0;     // "ns", "uri1" moved to docid 0.
+  document_id_old_to_new[100] = 99;  // child doc 100 becomes 99.
+  ASSERT_NO_FATAL_FAILURE(OptimizeDocumentStore());
+
+  DocumentId new_last_added_document_id = 99;
+  ICING_ASSERT_OK(index->Optimize(document_store_.get(), document_id_old_to_new,
+                                  /*namespace_id_old_to_new=*/{},
+                                  new_last_added_document_id));
+
+  // The new document with docid 0 is the old document 1, which was not a
+  // parent.
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(0),
+              IsOkAndHolds(IsEmpty()));
+
+  // Migrating the parent to a new doc id and check the child is still there.
+  DocumentId new_parent_doc_id = 1;
+  ICING_ASSERT_OK(index->MigrateParent(parent_qualified_id, new_parent_doc_id));
+  DocumentJoinIdPair new_child_join_id_pair(/*document_id=*/99,
+                                            /*joinable_property_id=*/0);
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(new_parent_doc_id),
+              IsOkAndHolds(ElementsAre(new_child_join_id_pair)));
+}
+
+TEST_P(QualifiedIdJoinIndexImplV3Test,
+       OptimizeShouldAllowAddingChildrenToDeletedParents) {
+  if (!feature_flags_->enable_non_existent_qualified_id_join()) {
+    return;
+  }
+
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  QualifiedId parent_qualified_id("ns", "uri0");
+  // Doc id 0
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
+  ASSERT_THAT(document_store_->GetDocumentId("ns", "uri0"), IsOkAndHolds(0));
+
+  // Index a join relation.
+  DocumentJoinIdPair child1(/*document_id=*/100, /*joinable_property_id=*/0);
+  ICING_ASSERT_OK(index->Put(
+      document_store_.get(), child1,
+      /*parent_qualified_ids=*/std::vector<QualifiedId>{parent_qualified_id}));
+
+  // Delete the parent document.
+  ICING_ASSERT_OK(document_store_->Delete("ns", "uri0", /*current_time_ms=*/0));
+
+  // Run Optimize.
+  std::vector<DocumentId> document_id_old_to_new(101, kInvalidDocumentId);
+  document_id_old_to_new[100] = 99;  // child doc 100 becomes 99.
+  ASSERT_NO_FATAL_FAILURE(OptimizeDocumentStore());
+
+  DocumentId new_last_added_document_id = 99;
+  ICING_ASSERT_OK(index->Optimize(document_store_.get(), document_id_old_to_new,
+                                  /*namespace_id_old_to_new=*/{},
+                                  new_last_added_document_id));
+
+  // Index a new child for the deleted parent.
+  DocumentJoinIdPair child2(/*document_id=*/101, /*joinable_property_id=*/0);
+  ICING_ASSERT_OK(index->Put(
+      document_store_.get(), child2,
+      /*parent_qualified_ids=*/std::vector<QualifiedId>{parent_qualified_id}));
+
+  // Migrating the parent to a new doc id and check both children are there.
+  DocumentId new_parent_doc_id = 1;
+  ICING_ASSERT_OK(index->MigrateParent(parent_qualified_id, new_parent_doc_id));
+  DocumentJoinIdPair remapped_child1(/*document_id=*/99,
+                                     /*joinable_property_id=*/0);
+  ICING_ASSERT_OK_AND_ASSIGN(
+      auto view, index->GetDocumentJoinIdPairArrayView(new_parent_doc_id));
+  EXPECT_THAT(view, ElementsAre(remapped_child1, child2));
+}
+
+TEST_P(QualifiedIdJoinIndexImplV3Test, Clear) {
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
                              QualifiedIdJoinIndexImplV3::Create(
@@ -1534,9 +2283,12 @@
   EXPECT_THAT(index->Clear(), IsOk());
   EXPECT_THAT(index, Pointee(IsEmpty()));
   EXPECT_THAT(index->last_added_document_id(), Eq(kInvalidDocumentId));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/0), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/1), IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/2), IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/0),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/1),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/2),
+              IsOkAndHolds(IsEmpty()));
 
   // Join index should be able to work normally after Clear().
   EXPECT_THAT(index->Put(child_join_id_pair4,
@@ -1546,7 +2298,7 @@
 
   EXPECT_THAT(index, Pointee(SizeIs(1)));
   EXPECT_THAT(index->last_added_document_id(), Eq(105));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/5),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/5),
               IsOkAndHolds(ElementsAre(child_join_id_pair4)));
 
   ICING_ASSERT_OK(index->PersistToDisk());
@@ -1558,10 +2310,30 @@
                                                 *feature_flags_));
   EXPECT_THAT(index, Pointee(SizeIs(1)));
   EXPECT_THAT(index->last_added_document_id(), Eq(105));
-  EXPECT_THAT(index->Get(/*parent_document_id=*/5),
+  EXPECT_THAT(index->GetDocumentJoinIdPairArrayView(/*parent_document_id=*/5),
               IsOkAndHolds(ElementsAre(child_join_id_pair4)));
 }
 
+TEST_P(QualifiedIdJoinIndexImplV3Test, V2ApiShouldBeUnimplemented) {
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  EXPECT_THAT(
+      index->Put(/*schema_type_id=*/0, /*joinable_property_id=*/0,
+                 /*document_id=*/0, /*ref_namespace_id_uri_fingerprints=*/{}),
+      StatusIs(libtextclassifier3::StatusCode::UNIMPLEMENTED));
+
+  EXPECT_THAT(index->GetIterator(/*schema_type_id=*/0,
+                                 /*joinable_property_id=*/0),
+              StatusIs(libtextclassifier3::StatusCode::UNIMPLEMENTED));
+}
+
+INSTANTIATE_TEST_SUITE_P(QualifiedIdJoinIndexImplV3Test,
+                         QualifiedIdJoinIndexImplV3Test,
+                         // Parameter: enable_non_existent_qualified_id_join
+                         testing::Values(true, false));
+
 }  // namespace
 
 }  // namespace lib
diff --git a/icing/join/qualified-id-join-index.h b/icing/join/qualified-id-join-index.h
index 55427c1..0349a15 100644
--- a/icing/join/qualified-id-join-index.h
+++ b/icing/join/qualified-id-join-index.h
@@ -18,7 +18,6 @@
 #include <cstdint>
 #include <memory>
 #include <string>
-#include <string_view>
 #include <utility>
 #include <vector>
 
@@ -28,9 +27,11 @@
 #include "icing/file/persistent-storage.h"
 #include "icing/join/document-id-to-join-info.h"
 #include "icing/join/document-join-id-pair.h"
+#include "icing/join/qualified-id.h"
 #include "icing/schema/joinable-property.h"
 #include "icing/store/document-filter-data.h"
 #include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
 #include "icing/store/namespace-id-fingerprint.h"
 #include "icing/store/namespace-id.h"
 #include "icing/util/crc32.h"
@@ -52,6 +53,37 @@
         const = 0;
   };
 
+  // An STL style array view object of DocumentJoinIdPairs for a given parent
+  // document id. This is only used in V3.
+  class DocumentJoinIdPairArrayView {
+   public:
+    using value_type = DocumentJoinIdPair;
+    using iterator = const DocumentJoinIdPair*;
+    using const_iterator = const DocumentJoinIdPair*;
+
+    explicit DocumentJoinIdPairArrayView(const DocumentJoinIdPair* data,
+                                         int size)
+        : data_(data), size_(size) {}
+
+    const DocumentJoinIdPair* data() const { return data_; }
+
+    int size() const { return size_; }
+
+    bool empty() const { return size_ == 0 || data_ == nullptr; }
+
+    iterator begin() { return data_; }
+    iterator end() { return data_ + size_; }
+
+    const_iterator begin() const { return data_; }
+    const_iterator end() const { return data_ + size_; }
+
+    const DocumentJoinIdPair& operator[](int idx) const { return data_[idx]; }
+
+   private:
+    const DocumentJoinIdPair* data_;
+    int size_;
+  };
+
   enum class Version { kV2, kV3 };
 
   static constexpr WorkingPathType kWorkingPathType =
@@ -84,8 +116,8 @@
       std::vector<NamespaceIdFingerprint>&&
           ref_namespace_id_uri_fingerprints) = 0;
 
-  // (v3 only) Puts a new child document and its referenced parent documents
-  // into the join index.
+  // (v3 only, deprecated) Puts a new child document and its referenced parent
+  // documents into the join index.
   //
   // Returns:
   //   - OK on success
@@ -95,6 +127,18 @@
       const DocumentJoinIdPair& child_document_join_id_pair,
       std::vector<DocumentId>&& parent_document_ids) = 0;
 
+  // (v3 only) Puts new join data into the index using qualified ids: adds a new
+  // child document and its referenced parent documents into the join index.
+  //
+  // Returns:
+  //   - OK on success
+  //   - INVALID_ARGUMENT_ERROR if child_document_join_id_pair is invalid
+  //   - Any FileBackedVector or KeyMapper errors
+  virtual libtextclassifier3::Status Put(
+      const DocumentStore* document_store,
+      const DocumentJoinIdPair& child_document_join_id_pair,
+      std::vector<QualifiedId>&& parent_qualified_ids) = 0;
+
   // (v2 only) Returns a JoinDataIterator for iterating through all join data of
   // the specified (schema_type_id, joinable_property_id).
   //
@@ -107,14 +151,16 @@
   GetIterator(SchemaTypeId schema_type_id,
               JoinablePropertyId joinable_property_id) const = 0;
 
-  // (v3 only) Gets the list of joinable children for the given parent document
-  // id.
+  // (v3 only) Gets an array view of all joinable children for the given parent
+  // document id.
   //
   // Returns:
-  //   - A list of children's DocumentJoinIdPair on success
+  //   - A DocumentJoinIdPairArrayView object on success. If there is no edge
+  //     for a valid node_id, then an array view with data() == nullptr and
+  //     size() == 0 will be returned
   //   - Any FileBackedVector errors
-  virtual libtextclassifier3::StatusOr<std::vector<DocumentJoinIdPair>> Get(
-      DocumentId parent_document_id) const = 0;
+  virtual libtextclassifier3::StatusOr<DocumentJoinIdPairArrayView>
+  GetDocumentJoinIdPairArrayView(DocumentId parent_document_id) const = 0;
 
   // Migrates existing join data for a parent document from old_document_id to
   // new_document_id if necessary.
@@ -126,6 +172,18 @@
   virtual libtextclassifier3::Status MigrateParent(
       DocumentId old_document_id, DocumentId new_document_id) = 0;
 
+  // Migrates existing join data for a parent document from its qualified id to
+  // a new document id. This is used when a document that was previously
+  // referenced as a "missing parent" (by its qualified id) is indexed and
+  // assigned a document id.
+  //
+  // Returns:
+  //   - OK on success
+  //   - INVALID_ARGUMENT_ERROR if new_document_id is invalid
+  //   - Any errors, depending on the implementation
+  virtual libtextclassifier3::Status MigrateParent(
+      const QualifiedId& parent_qualified_id, DocumentId new_document_id) = 0;
+
   // Reduces internal file sizes by reclaiming space and ids of deleted
   // documents. Qualified id type joinable index will convert all entries to the
   // new document ids.
@@ -144,6 +202,7 @@
   //     an invalid state and the caller should handle it properly (e.g. discard
   //     and rebuild)
   virtual libtextclassifier3::Status Optimize(
+      const DocumentStore* document_store,
       const std::vector<DocumentId>& document_id_old_to_new,
       const std::vector<NamespaceId>& namespace_id_old_to_new,
       DocumentId new_last_added_document_id) = 0;
diff --git a/icing/join/qualified-id-join-indexing-handler-v2_test.cc b/icing/join/qualified-id-join-indexing-handler-v2_test.cc
index b131efc..d1de824 100644
--- a/icing/join/qualified-id-join-indexing-handler-v2_test.cc
+++ b/icing/join/qualified-id-join-indexing-handler-v2_test.cc
@@ -32,6 +32,7 @@
 #include "icing/join/qualified-id-join-index.h"
 #include "icing/join/qualified-id-join-indexing-handler.h"
 #include "icing/join/qualified-id.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
@@ -153,14 +154,18 @@
                 IsTrue());
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, doc_store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, doc_store_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     doc_store_ = std::move(create_result.document_store);
 
     // Get FakeType related ids.
@@ -242,20 +247,20 @@
 
 TEST_F(QualifiedIdJoinIndexingHandlerV2Test,
        CreationWithNullPointerShouldFail) {
-  EXPECT_THAT(
-      QualifiedIdJoinIndexingHandler::Create(
-          /*clock=*/nullptr, doc_store_.get(), qualified_id_join_index_.get()),
-      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(QualifiedIdJoinIndexingHandler::Create(
+                  /*clock=*/nullptr, doc_store_.get(),
+                  qualified_id_join_index_.get(), feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 
-  EXPECT_THAT(
-      QualifiedIdJoinIndexingHandler::Create(
-          &fake_clock_, /*doc_store=*/nullptr, qualified_id_join_index_.get()),
-      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(QualifiedIdJoinIndexingHandler::Create(
+                  &fake_clock_, /*doc_store=*/nullptr,
+                  qualified_id_join_index_.get(), feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 
-  EXPECT_THAT(
-      QualifiedIdJoinIndexingHandler::Create(
-          &fake_clock_, doc_store_.get(), /*qualified_id_join_index=*/nullptr),
-      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(QualifiedIdJoinIndexingHandler::Create(
+                  &fake_clock_, doc_store_.get(),
+                  /*qualified_id_join_index=*/nullptr, feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 }
 
 TEST_F(QualifiedIdJoinIndexingHandlerV2Test, HandleJoinableProperty) {
@@ -267,14 +272,25 @@
           .SetSchema(std::string(kReferencedType))
           .AddStringProperty(std::string(kPropertyName), "one")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store_->Put(referenced_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument ref_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(referenced_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store_->Put(ref_tokenized_document.document_wrapper()));
   DocumentId ref_doc_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       NamespaceId ref_doc_ns_id,
-      doc_store_->GetNamespaceId(referenced_document.namespace_()));
+      doc_store_->GetNamespaceId(
+          ref_tokenized_document.document_wrapper().document().namespace_()));
   NamespaceIdFingerprint ref_doc_nsid_uri_fingerprint(
-      /*namespace_id=*/ref_doc_ns_id, /*target_str=*/referenced_document.uri());
+      /*namespace_id=*/ref_doc_ns_id,
+      /*target_str=*/ref_tokenized_document.document_wrapper()
+          .document()
+          .uri());
   ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_nsid_uri_fingerprint),
               IsOkAndHolds(ref_doc_id));
 
@@ -286,12 +302,15 @@
           .AddStringProperty(std::string(kPropertyQualifiedId),
                              "pkg$db/ns#ref_type/1")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, doc_store_->Put(document));
-  DocumentId doc_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result, doc_store_->Put(tokenized_document.document_wrapper()));
+  DocumentId doc_id = put_result.new_document_id;
 
   // Handle document.
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
@@ -299,7 +318,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
   EXPECT_THAT(
       handler->Handle(tokenized_document, doc_id, put_result.old_document_id,
                       /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
@@ -326,15 +346,25 @@
           .SetSchema(std::string(kReferencedType))
           .AddStringProperty(std::string(kPropertyName), "one")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store_->Put(referenced_document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument ref_tokenized_document1,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(referenced_document1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store_->Put(ref_tokenized_document1.document_wrapper()));
   DocumentId ref_doc_id1 = put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       NamespaceId ref_doc_ns_id1,
-      doc_store_->GetNamespaceId(referenced_document1.namespace_()));
+      doc_store_->GetNamespaceId(
+          ref_tokenized_document1.document_wrapper().document().namespace_()));
   NamespaceIdFingerprint ref_doc_nsid_uri_fingerprint1(
       /*namespace_id=*/ref_doc_ns_id1,
-      /*target_str=*/referenced_document1.uri());
+      /*target_str=*/ref_tokenized_document1.document_wrapper()
+          .document()
+          .uri());
   ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_nsid_uri_fingerprint1),
               IsOkAndHolds(ref_doc_id1));
 
@@ -346,15 +376,25 @@
           .SetSchema(std::string(kReferencedType))
           .AddStringProperty(std::string(kPropertyName), "two")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store_->Put(referenced_document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument ref_tokenized_document2,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(referenced_document2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store_->Put(ref_tokenized_document2.document_wrapper()));
   DocumentId ref_doc_id2 = put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       NamespaceId ref_doc_ns_id2,
-      doc_store_->GetNamespaceId(referenced_document2.namespace_()));
+      doc_store_->GetNamespaceId(
+          ref_tokenized_document2.document_wrapper().document().namespace_()));
   NamespaceIdFingerprint ref_doc_nsid_uri_fingerprint2(
       /*namespace_id=*/ref_doc_ns_id2,
-      /*target_str=*/referenced_document2.uri());
+      /*target_str=*/ref_tokenized_document2.document_wrapper()
+          .document()
+          .uri());
   ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_nsid_uri_fingerprint2),
               IsOkAndHolds(ref_doc_id2));
 
@@ -378,25 +418,29 @@
           .AddStringProperty(std::string(kPropertyQualifiedId2),
                              "pkg$db/ns#ref_type/1")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store_->Put(nested_document));
-  DocumentId doc_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
-      TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                nested_document));
+      TokenizedDocument nested_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(nested_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store_->Put(nested_tokenized_document.document_wrapper()));
+  DocumentId doc_id = put_result.new_document_id;
 
-  // Handle nested_document.
+  // Handle nested_tokenized_document.
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(kInvalidDocumentId));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
-  EXPECT_THAT(
-      handler->Handle(tokenized_document, doc_id, put_result.old_document_id,
-                      /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
-      IsOk());
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
+  EXPECT_THAT(handler->Handle(
+                  nested_tokenized_document, doc_id, put_result.old_document_id,
+                  /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
+              IsOk());
 
   // Verify the state of qualified_id_join_index_ after Handle().
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(), Eq(doc_id));
@@ -440,13 +484,16 @@
           .AddStringProperty(std::string(kPropertyQualifiedId),
                              std::string(kInvalidFormatQualifiedId))
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store_->Put(document));
-  DocumentId doc_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store_->Put(tokenized_document.document_wrapper()));
+  DocumentId doc_id = put_result.new_document_id;
 
   // Handle document. Should ignore invalid format qualified id.
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
@@ -454,7 +501,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
   EXPECT_THAT(
       handler->Handle(tokenized_document, doc_id, put_result.old_document_id,
                       /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
@@ -484,13 +532,16 @@
               std::string(kPropertyQualifiedId),
               absl_ports::StrCat(kUnknownNamespace, "#", "ref_type/1"))
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store_->Put(document));
-  DocumentId doc_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store_->Put(tokenized_document.document_wrapper()));
+  DocumentId doc_id = put_result.new_document_id;
 
   // Handle document.
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
@@ -498,7 +549,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
   EXPECT_THAT(
       handler->Handle(tokenized_document, doc_id, put_result.old_document_id,
                       /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
@@ -520,13 +572,17 @@
                                .SetKey("icing", "fake_type/1")
                                .SetSchema(std::string(kFakeType))
                                .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store_->Put(document));
-  DocumentId doc_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store_->Put(tokenized_document.document_wrapper()));
+  DocumentId doc_id = put_result.new_document_id;
+
   ASSERT_THAT(tokenized_document.qualified_id_join_properties(), IsEmpty());
 
   // Handle document.
@@ -535,7 +591,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
   EXPECT_THAT(
       handler->Handle(tokenized_document, doc_id, put_result.old_document_id,
                       /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
@@ -562,14 +619,25 @@
           .SetSchema(std::string(kReferencedType))
           .AddStringProperty(std::string(kPropertyName), "one")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store_->Put(referenced_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument ref_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(referenced_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store_->Put(ref_tokenized_document.document_wrapper()));
   DocumentId ref_doc_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       NamespaceId ref_doc_ns_id,
-      doc_store_->GetNamespaceId(referenced_document.namespace_()));
+      doc_store_->GetNamespaceId(
+          ref_tokenized_document.document_wrapper().document().namespace_()));
   NamespaceIdFingerprint ref_doc_nsid_uri_fingerprint(
-      /*namespace_id=*/ref_doc_ns_id, /*target_str=*/referenced_document.uri());
+      /*namespace_id=*/ref_doc_ns_id,
+      /*target_str=*/ref_tokenized_document.document_wrapper()
+          .document()
+          .uri());
   ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_nsid_uri_fingerprint),
               IsOkAndHolds(ref_doc_id));
 
@@ -581,11 +649,13 @@
           .AddStringProperty(std::string(kPropertyQualifiedId),
                              "pkg$db/ns#ref_type/1")
           .Build();
-  ICING_ASSERT_OK(doc_store_->Put(document));
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
+  ICING_ASSERT_OK(doc_store_->Put(tokenized_document.document_wrapper()));
 
   qualified_id_join_index_->set_last_added_document_id(ref_doc_id);
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
@@ -594,7 +664,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
 
   // Handling document with kInvalidDocumentId should cause a failure.
   EXPECT_THAT(
@@ -637,14 +708,25 @@
           .SetSchema(std::string(kReferencedType))
           .AddStringProperty(std::string(kPropertyName), "one")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store_->Put(referenced_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument ref_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(referenced_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store_->Put(ref_tokenized_document.document_wrapper()));
   DocumentId ref_doc_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       NamespaceId ref_doc_ns_id,
-      doc_store_->GetNamespaceId(referenced_document.namespace_()));
+      doc_store_->GetNamespaceId(
+          ref_tokenized_document.document_wrapper().document().namespace_()));
   NamespaceIdFingerprint ref_doc_nsid_uri_fingerprint(
-      /*namespace_id=*/ref_doc_ns_id, /*target_str=*/referenced_document.uri());
+      /*namespace_id=*/ref_doc_ns_id,
+      /*target_str=*/ref_tokenized_document.document_wrapper()
+          .document()
+          .uri());
   ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_nsid_uri_fingerprint),
               IsOkAndHolds(ref_doc_id));
 
@@ -656,17 +738,21 @@
           .AddStringProperty(std::string(kPropertyQualifiedId),
                              "pkg$db/ns#ref_type/1")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, doc_store_->Put(document));
-  DocumentId doc_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result, doc_store_->Put(tokenized_document.document_wrapper()));
+  DocumentId doc_id = put_result.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
 
   // Handling document with document_id == last_added_document_id should cause a
   // failure.
@@ -715,14 +801,25 @@
           .SetSchema(std::string(kReferencedType))
           .AddStringProperty(std::string(kPropertyName), "one")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store_->Put(referenced_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument ref_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(referenced_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store_->Put(ref_tokenized_document.document_wrapper()));
   DocumentId ref_doc_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       NamespaceId ref_doc_ns_id,
-      doc_store_->GetNamespaceId(referenced_document.namespace_()));
+      doc_store_->GetNamespaceId(
+          ref_tokenized_document.document_wrapper().document().namespace_()));
   NamespaceIdFingerprint ref_doc_nsid_uri_fingerprint(
-      /*namespace_id=*/ref_doc_ns_id, /*target_str=*/referenced_document.uri());
+      /*namespace_id=*/ref_doc_ns_id,
+      /*target_str=*/ref_tokenized_document.document_wrapper()
+          .document()
+          .uri());
   ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_nsid_uri_fingerprint),
               IsOkAndHolds(ref_doc_id));
 
@@ -734,17 +831,21 @@
           .AddStringProperty(std::string(kPropertyQualifiedId),
                              "pkg$db/ns#ref_type/1")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, doc_store_->Put(document));
-  DocumentId doc_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result, doc_store_->Put(tokenized_document.document_wrapper()));
+  DocumentId doc_id = put_result.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
 
   // Handle document with document_id > last_added_document_id in recovery mode.
   // The handler should index this document and update last_added_document_id.
@@ -774,14 +875,25 @@
           .SetSchema(std::string(kReferencedType))
           .AddStringProperty(std::string(kPropertyName), "one")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store_->Put(referenced_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument ref_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(referenced_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store_->Put(ref_tokenized_document.document_wrapper()));
   DocumentId ref_doc_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       NamespaceId ref_doc_ns_id,
-      doc_store_->GetNamespaceId(referenced_document.namespace_()));
+      doc_store_->GetNamespaceId(
+          ref_tokenized_document.document_wrapper().document().namespace_()));
   NamespaceIdFingerprint ref_doc_nsid_uri_fingerprint(
-      /*namespace_id=*/ref_doc_ns_id, /*target_str=*/referenced_document.uri());
+      /*namespace_id=*/ref_doc_ns_id,
+      /*target_str=*/ref_tokenized_document.document_wrapper()
+          .document()
+          .uri());
   ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_nsid_uri_fingerprint),
               IsOkAndHolds(ref_doc_id));
 
@@ -793,17 +905,21 @@
           .AddStringProperty(std::string(kPropertyQualifiedId),
                              "pkg$db/ns#ref_type/1")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, doc_store_->Put(document));
-  DocumentId doc_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result, doc_store_->Put(tokenized_document.document_wrapper()));
+  DocumentId doc_id = put_result.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
 
   // Handle document with document_id == last_added_document_id in recovery
   // mode. We should not get any error, but the handler should ignore the
diff --git a/icing/join/qualified-id-join-indexing-handler-v3_test.cc b/icing/join/qualified-id-join-indexing-handler-v3_test.cc
index 7649424..cf70689 100644
--- a/icing/join/qualified-id-join-indexing-handler-v3_test.cc
+++ b/icing/join/qualified-id-join-indexing-handler-v3_test.cc
@@ -29,6 +29,7 @@
 #include "icing/join/qualified-id-join-index-impl-v3.h"
 #include "icing/join/qualified-id-join-indexing-handler.h"
 #include "icing/join/qualified-id.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
@@ -77,10 +78,32 @@
 static constexpr std::string_view kPropertyNestedDoc = "nested";
 static constexpr std::string_view kPropertyQualifiedId2 = "qualifiedId2";
 
-class QualifiedIdJoinIndexingHandlerV3Test : public ::testing::Test {
+class QualifiedIdJoinIndexingHandlerV3Test
+    : public ::testing::TestWithParam<bool> {
  protected:
   void SetUp() override {
-    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+    feature_flags_ = std::make_unique<FeatureFlags>(
+        /*enable_circular_schema_definitions=*/true,
+        /*enable_scorable_properties=*/true,
+        /*enable_embedding_quantization=*/true,
+        /*enable_repeated_field_joins=*/true,
+        /*enable_embedding_backup_generation=*/true,
+        /*enable_schema_database=*/true,
+        /*release_backup_schema_file_if_overlay_present=*/true,
+        /*enable_strict_page_byte_size_limit=*/true,
+        /*enable_smaller_decompression_buffer_size=*/true,
+        /*enable_eigen_embedding_scoring=*/true,
+        /*enable_passing_filter_to_children=*/true,
+        /*enable_proto_log_new_header_format=*/true,
+        /*enable_embedding_iterator_v2=*/true,
+        /*enable_reusable_decompression_buffer=*/true,
+        /*enable_schema_type_id_optimization=*/true,
+        /*enable_optimize_improvements=*/true,
+        /*expired_document_purge_threshold_ms=*/0,
+        /*enable_non_existent_qualified_id_join=*/GetParam(),
+        /*enable_skip_set_schema_type_equality_check=*/true,
+        /*enable_embed_query_optimization=*/true,
+        /*enable_schema_definition_deduping=*/true);
 
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
@@ -127,7 +150,7 @@
                 PropertyConfigBuilder()
                     .SetName(kPropertyQualifiedId)
                     .SetDataTypeJoinableString(JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                    .SetCardinality(CARDINALITY_OPTIONAL)))
+                    .SetCardinality(CARDINALITY_REPEATED)))
             .AddType(
                 SchemaTypeConfigBuilder()
                     .SetType(kNestedType)
@@ -150,14 +173,18 @@
                 IsTrue());
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, doc_store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, doc_store_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     doc_store_ = std::move(create_result.document_store);
 
     // Get FakeType related ids.
@@ -220,25 +247,30 @@
   JoinablePropertyId nested_type_joinable_property_id_;
 };
 
-TEST_F(QualifiedIdJoinIndexingHandlerV3Test,
+TEST_P(QualifiedIdJoinIndexingHandlerV3Test,
        CreationWithNullPointerShouldFail) {
-  EXPECT_THAT(
-      QualifiedIdJoinIndexingHandler::Create(
-          /*clock=*/nullptr, doc_store_.get(), qualified_id_join_index_.get()),
-      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(QualifiedIdJoinIndexingHandler::Create(
+                  /*clock=*/nullptr, doc_store_.get(),
+                  qualified_id_join_index_.get(), feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 
-  EXPECT_THAT(
-      QualifiedIdJoinIndexingHandler::Create(
-          &fake_clock_, /*doc_store=*/nullptr, qualified_id_join_index_.get()),
-      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(QualifiedIdJoinIndexingHandler::Create(
+                  &fake_clock_, /*doc_store=*/nullptr,
+                  qualified_id_join_index_.get(), feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 
-  EXPECT_THAT(
-      QualifiedIdJoinIndexingHandler::Create(
-          &fake_clock_, doc_store_.get(), /*qualified_id_join_index=*/nullptr),
-      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(QualifiedIdJoinIndexingHandler::Create(
+                  &fake_clock_, doc_store_.get(),
+                  /*qualified_id_join_index=*/nullptr, feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+
+  EXPECT_THAT(QualifiedIdJoinIndexingHandler::Create(
+                  &fake_clock_, doc_store_.get(),
+                  qualified_id_join_index_.get(), /*feature_flags=*/nullptr),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerV3Test, HandleJoinableProperty) {
+TEST_P(QualifiedIdJoinIndexingHandlerV3Test, HandleJoinableProperty) {
   // Create and put parent document.
   DocumentProto parent_document =
       DocumentBuilder()
@@ -246,8 +278,15 @@
           .SetSchema(std::string(kReferencedType))
           .AddStringProperty(std::string(kPropertyName), "one")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult parent_put_result,
-                             doc_store_->Put(parent_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument parent_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(parent_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult parent_put_result,
+      doc_store_->Put(parent_tokenized_document.document_wrapper()));
 
   // Create and put child document. Also tokenize it.
   DocumentProto child_document =
@@ -257,12 +296,15 @@
           .AddStringProperty(std::string(kPropertyQualifiedId),
                              "pkg$db/ns#ref_type/1")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult child_put_result,
-                             doc_store_->Put(child_document));
   ICING_ASSERT_OK_AND_ASSIGN(
-      TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(child_document)));
+      TokenizedDocument child_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(child_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult child_put_result,
+      doc_store_->Put(child_tokenized_document.document_wrapper()));
 
   // Handle document.
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
@@ -270,24 +312,26 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
-  EXPECT_THAT(
-      handler->Handle(tokenized_document, child_put_result.new_document_id,
-                      child_put_result.old_document_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
-      IsOk());
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
+  EXPECT_THAT(handler->Handle(
+                  child_tokenized_document, child_put_result.new_document_id,
+                  child_put_result.old_document_id, /*recovery_mode=*/false,
+                  /*put_document_stats=*/nullptr),
+              IsOk());
 
   // Verify the state of qualified_id_join_index_ after Handle().
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(child_put_result.new_document_id));
   EXPECT_THAT(qualified_id_join_index_, Pointee(SizeIs(1)));
   EXPECT_THAT(
-      qualified_id_join_index_->Get(parent_put_result.new_document_id),
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+          parent_put_result.new_document_id),
       IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
           child_put_result.new_document_id, fake_type_joinable_property_id_))));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerV3Test, HandleNestedJoinableProperty) {
+TEST_P(QualifiedIdJoinIndexingHandlerV3Test, HandleNestedJoinableProperty) {
   // Create and put parent document1. Get its document id and namespace id.
   DocumentProto parent_document1 =
       DocumentBuilder()
@@ -295,8 +339,15 @@
           .SetSchema(std::string(kReferencedType))
           .AddStringProperty(std::string(kPropertyName), "one")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult parent_put_result1,
-                             doc_store_->Put(parent_document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument parent_tokenized_document1,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(parent_document1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult parent_put_result1,
+      doc_store_->Put(parent_tokenized_document1.document_wrapper()));
 
   // Create and put parent document2.
   DocumentProto parent_document2 =
@@ -305,8 +356,15 @@
           .SetSchema(std::string(kReferencedType))
           .AddStringProperty(std::string(kPropertyName), "two")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult parent_put_result2,
-                             doc_store_->Put(parent_document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument parent_tokenized_document2,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(parent_document2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult parent_put_result2,
+      doc_store_->Put(parent_tokenized_document2.document_wrapper()));
 
   // Create and put child document:
   // - kPropertyNestedDoc.kPropertyQualifiedId refers to parent_document2.
@@ -328,12 +386,15 @@
           .AddStringProperty(std::string(kPropertyQualifiedId2),
                              "pkg$db/ns#ref_type/1")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult child_put_result,
-                             doc_store_->Put(child_document));
   ICING_ASSERT_OK_AND_ASSIGN(
-      TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                child_document));
+      TokenizedDocument child_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(child_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult child_put_result,
+      doc_store_->Put(child_tokenized_document.document_wrapper()));
 
   // Handle nested_document.
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
@@ -341,28 +402,31 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
-  EXPECT_THAT(
-      handler->Handle(tokenized_document, child_put_result.new_document_id,
-                      child_put_result.old_document_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
-      IsOk());
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
+  EXPECT_THAT(handler->Handle(
+                  child_tokenized_document, child_put_result.new_document_id,
+                  child_put_result.old_document_id, /*recovery_mode=*/false,
+                  /*put_document_stats=*/nullptr),
+              IsOk());
 
   // Verify the state of qualified_id_join_index_ after Handle().
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(child_put_result.new_document_id));
   EXPECT_THAT(qualified_id_join_index_, Pointee(SizeIs(2)));
-  EXPECT_THAT(qualified_id_join_index_->Get(parent_put_result1.new_document_id),
+  EXPECT_THAT(qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+                  parent_put_result1.new_document_id),
               IsOkAndHolds(ElementsAre(
                   DocumentJoinIdPair(child_put_result.new_document_id,
                                      nested_type_joinable_property_id_))));
-  EXPECT_THAT(qualified_id_join_index_->Get(parent_put_result2.new_document_id),
+  EXPECT_THAT(qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+                  parent_put_result2.new_document_id),
               IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
                   child_put_result.new_document_id,
                   nested_type_nested_joinable_property_id_))));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerV3Test,
+TEST_P(QualifiedIdJoinIndexingHandlerV3Test,
        HandleShouldSkipInvalidFormatQualifiedId) {
   static constexpr std::string_view kInvalidFormatQualifiedId =
       "invalid_format_qualified_id";
@@ -378,12 +442,15 @@
           .AddStringProperty(std::string(kPropertyQualifiedId),
                              std::string(kInvalidFormatQualifiedId))
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult child_put_result,
-                             doc_store_->Put(document));
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult child_put_result,
+      doc_store_->Put(tokenized_document.document_wrapper()));
 
   // Handle document. Should ignore invalid format qualified id.
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
@@ -391,7 +458,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
   EXPECT_THAT(
       handler->Handle(tokenized_document, child_put_result.new_document_id,
                       child_put_result.old_document_id, /*recovery_mode=*/false,
@@ -406,19 +474,22 @@
   EXPECT_THAT(qualified_id_join_index_, Pointee(IsEmpty()));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerV3Test, HandleShouldSkipEmptyQualifiedId) {
+TEST_P(QualifiedIdJoinIndexingHandlerV3Test, HandleShouldSkipEmptyQualifiedId) {
   // Create and put child document without any qualified id. Also tokenize it.
   DocumentProto document = DocumentBuilder()
                                .SetKey("icing", "fake_type/1")
                                .SetSchema(std::string(kFakeType))
                                .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult child_put_result,
-                             doc_store_->Put(document));
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(document)));
   ASSERT_THAT(tokenized_document.qualified_id_join_properties(), IsEmpty());
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult child_put_result,
+      doc_store_->Put(tokenized_document.document_wrapper()));
 
   // Handle document.
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
@@ -426,7 +497,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
   EXPECT_THAT(
       handler->Handle(tokenized_document, child_put_result.new_document_id,
                       child_put_result.old_document_id,
@@ -441,7 +513,7 @@
   EXPECT_THAT(qualified_id_join_index_, Pointee(IsEmpty()));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerV3Test, HandleShouldMigrateParent) {
+TEST_P(QualifiedIdJoinIndexingHandlerV3Test, HandleShouldMigrateParent) {
   // Create and put parent document.
   DocumentProto parent_document =
       DocumentBuilder()
@@ -449,8 +521,15 @@
           .SetSchema(std::string(kReferencedType))
           .AddStringProperty(std::string(kPropertyName), "one")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult parent_put_result,
-                             doc_store_->Put(parent_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument parent_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(parent_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult parent_put_result,
+      doc_store_->Put(parent_tokenized_document.document_wrapper()));
 
   // Create and put child and grandchild document with relations:
   // parent_document <- child_document <- grandchild_document
@@ -469,33 +548,43 @@
           .AddStringProperty(std::string(kPropertyQualifiedId),
                              "icing#fake_type/1")
           .Build();
+  DocumentProto child_document_replaced = child_document;
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
 
   // Put and index child document.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult child_put_result,
-                             doc_store_->Put(child_document));
-  ASSERT_THAT(child_put_result.old_document_id, Eq(kInvalidDocumentId));
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument child_tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                child_document));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(child_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult child_put_result,
+      doc_store_->Put(child_tokenized_document.document_wrapper()));
+  ASSERT_THAT(child_put_result.old_document_id, Eq(kInvalidDocumentId));
+
   ICING_ASSERT_OK(handler->Handle(
       child_tokenized_document, child_put_result.new_document_id,
       child_put_result.old_document_id,
       /*recovery_mode=*/false, /*put_document_stats=*/nullptr));
 
   // Put and index grandchild document.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult grandchild_put_result,
-                             doc_store_->Put(grandchild_document));
-  ASSERT_THAT(grandchild_put_result.old_document_id, Eq(kInvalidDocumentId));
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument grandchild_tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(grandchild_document)));
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(grandchild_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult grandchild_put_result,
+      doc_store_->Put(grandchild_tokenized_document.document_wrapper()));
+  ASSERT_THAT(grandchild_put_result.old_document_id, Eq(kInvalidDocumentId));
+
   ICING_ASSERT_OK(handler->Handle(
       grandchild_tokenized_document, grandchild_put_result.new_document_id,
       grandchild_put_result.old_document_id,
@@ -507,46 +596,59 @@
               Eq(grandchild_put_result.new_document_id));
   ASSERT_THAT(qualified_id_join_index_, Pointee(SizeIs(2)));
   ASSERT_THAT(
-      qualified_id_join_index_->Get(parent_put_result.new_document_id),
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+          parent_put_result.new_document_id),
       IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
           child_put_result.new_document_id, fake_type_joinable_property_id_))));
-  ASSERT_THAT(qualified_id_join_index_->Get(child_put_result.new_document_id),
+  ASSERT_THAT(qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+                  child_put_result.new_document_id),
               IsOkAndHolds(ElementsAre(
                   DocumentJoinIdPair(grandchild_put_result.new_document_id,
                                      fake_type_joinable_property_id_))));
 
   // Update the child document and index it again.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult child_put_result2,
-                             doc_store_->Put(child_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument child_tokenized_document_replaced,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(child_document_replaced)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult child_put_result2,
+      doc_store_->Put(child_tokenized_document_replaced.document_wrapper()));
   ASSERT_THAT(child_put_result2.old_document_id,
               Eq(child_put_result.new_document_id));
 
   // Handle should migrate.
-  EXPECT_THAT(handler->Handle(
-                  child_tokenized_document, child_put_result2.new_document_id,
-                  child_put_result2.old_document_id, /*recovery_mode=*/false,
-                  /*put_document_stats=*/nullptr),
-              IsOk());
+  EXPECT_THAT(
+      handler->Handle(child_tokenized_document_replaced,
+                      child_put_result2.new_document_id,
+                      child_put_result2.old_document_id,
+                      /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
+      IsOk());
   EXPECT_THAT(qualified_id_join_index_, Pointee(SizeIs(3)));
   // Get() with parent document id should return DocumentJoinIdPairs for both
   // old and new child document id.
-  EXPECT_THAT(qualified_id_join_index_->Get(parent_put_result.new_document_id),
+  EXPECT_THAT(qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+                  parent_put_result.new_document_id),
               IsOkAndHolds(ElementsAre(
                   DocumentJoinIdPair(child_put_result.new_document_id,
                                      fake_type_joinable_property_id_),
                   DocumentJoinIdPair(child_put_result2.new_document_id,
                                      fake_type_joinable_property_id_))));
   // Get() with old child document id should return empty list.
-  EXPECT_THAT(qualified_id_join_index_->Get(child_put_result.new_document_id),
+  EXPECT_THAT(qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+                  child_put_result.new_document_id),
               IsOkAndHolds(IsEmpty()));
   // Get() with new child document id should return grandchild join id pair.
-  EXPECT_THAT(qualified_id_join_index_->Get(child_put_result2.new_document_id),
+  EXPECT_THAT(qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+                  child_put_result2.new_document_id),
               IsOkAndHolds(ElementsAre(
                   DocumentJoinIdPair(grandchild_put_result.new_document_id,
                                      fake_type_joinable_property_id_))));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerV3Test,
+TEST_P(QualifiedIdJoinIndexingHandlerV3Test,
        HandleInvalidNewDocumentIdShouldReturnInvalidArgumentError) {
   // Create and put parent document.
   DocumentProto parent_document =
@@ -555,23 +657,33 @@
           .SetSchema(std::string(kReferencedType))
           .AddStringProperty(std::string(kPropertyName), "one")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult parent_put_result,
-                             doc_store_->Put(parent_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument parent_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(parent_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult parent_put_result,
+      doc_store_->Put(parent_tokenized_document.document_wrapper()));
 
   // Create and put child document. Also tokenize it.
-  DocumentProto document =
+  DocumentProto child_document =
       DocumentBuilder()
           .SetKey("icing", "fake_type/1")
           .SetSchema(std::string(kFakeType))
           .AddStringProperty(std::string(kPropertyQualifiedId),
                              "pkg$db/ns#ref_type/1")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult child_put_result,
-                             doc_store_->Put(document));
   ICING_ASSERT_OK_AND_ASSIGN(
-      TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument child_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(child_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult child_put_result,
+      doc_store_->Put(child_tokenized_document.document_wrapper()));
 
   qualified_id_join_index_->set_last_added_document_id(
       parent_put_result.new_document_id);
@@ -581,11 +693,12 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
 
   // Handling document with kInvalidDocumentId should cause a failure.
   EXPECT_THAT(
-      handler->Handle(tokenized_document, kInvalidDocumentId,
+      handler->Handle(child_tokenized_document, kInvalidDocumentId,
                       child_put_result.old_document_id, /*recovery_mode=*/false,
                       /*put_document_stats=*/nullptr),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
@@ -597,18 +710,19 @@
 
   // Recovery mode should get the same result.
   EXPECT_THAT(
-      handler->Handle(tokenized_document, kInvalidDocumentId,
+      handler->Handle(child_tokenized_document, kInvalidDocumentId,
                       child_put_result.old_document_id, /*recovery_mode=*/true,
                       /*put_document_stats=*/nullptr),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(parent_put_result.new_document_id));
   EXPECT_THAT(qualified_id_join_index_, Pointee(IsEmpty()));
-  EXPECT_THAT(qualified_id_join_index_->Get(parent_put_result.new_document_id),
+  EXPECT_THAT(qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+                  parent_put_result.new_document_id),
               IsOkAndHolds(IsEmpty()));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerV3Test,
+TEST_P(QualifiedIdJoinIndexingHandlerV3Test,
        HandleOutOfOrderDocumentIdShouldReturnInvalidArgumentError) {
   // Create and put parent document.
   DocumentProto parent_document =
@@ -617,28 +731,39 @@
           .SetSchema(std::string(kReferencedType))
           .AddStringProperty(std::string(kPropertyName), "one")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult parent_put_result,
-                             doc_store_->Put(parent_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument parent_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(parent_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult parent_put_result,
+      doc_store_->Put(parent_tokenized_document.document_wrapper()));
 
   // Create and put child document. Also tokenize it.
-  DocumentProto document =
+  DocumentProto child_document =
       DocumentBuilder()
           .SetKey("icing", "fake_type/1")
           .SetSchema(std::string(kFakeType))
           .AddStringProperty(std::string(kPropertyQualifiedId),
                              "pkg$db/ns#ref_type/1")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult child_put_result,
-                             doc_store_->Put(document));
   ICING_ASSERT_OK_AND_ASSIGN(
-      TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument child_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(child_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult child_put_result,
+      doc_store_->Put(child_tokenized_document.document_wrapper()));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
 
   // Handling document with document_id == last_added_document_id should cause a
   // failure.
@@ -646,17 +771,18 @@
       child_put_result.new_document_id);
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(child_put_result.new_document_id));
-  EXPECT_THAT(
-      handler->Handle(tokenized_document, child_put_result.new_document_id,
-                      child_put_result.old_document_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
-      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(handler->Handle(
+                  child_tokenized_document, child_put_result.new_document_id,
+                  child_put_result.old_document_id, /*recovery_mode=*/false,
+                  /*put_document_stats=*/nullptr),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   // Verify the state of qualified_id_join_index_ after Handle(). Both index
   // data and last_added_document_id should remain unchanged.
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(child_put_result.new_document_id));
   EXPECT_THAT(qualified_id_join_index_, Pointee(IsEmpty()));
-  EXPECT_THAT(qualified_id_join_index_->Get(parent_put_result.new_document_id),
+  EXPECT_THAT(qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+                  parent_put_result.new_document_id),
               IsOkAndHolds(IsEmpty()));
 
   // Handling document with document_id < last_added_document_id should cause a
@@ -665,21 +791,22 @@
       child_put_result.new_document_id + 1);
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(child_put_result.new_document_id + 1));
-  EXPECT_THAT(
-      handler->Handle(tokenized_document, child_put_result.new_document_id,
-                      child_put_result.old_document_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
-      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(handler->Handle(
+                  child_tokenized_document, child_put_result.new_document_id,
+                  child_put_result.old_document_id, /*recovery_mode=*/false,
+                  /*put_document_stats=*/nullptr),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   // Verify the state of qualified_id_join_index_ after Handle(). Both index
   // data and last_added_document_id should remain unchanged.
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(child_put_result.new_document_id + 1));
   EXPECT_THAT(qualified_id_join_index_, Pointee(IsEmpty()));
-  EXPECT_THAT(qualified_id_join_index_->Get(parent_put_result.new_document_id),
+  EXPECT_THAT(qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+                  parent_put_result.new_document_id),
               IsOkAndHolds(IsEmpty()));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerV3Test,
+TEST_P(QualifiedIdJoinIndexingHandlerV3Test,
        HandleRecoveryModeShouldIndexDocsGtLastAddedDocId) {
   // Create and put parent document.
   DocumentProto parent_document =
@@ -688,28 +815,39 @@
           .SetSchema(std::string(kReferencedType))
           .AddStringProperty(std::string(kPropertyName), "one")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult parent_put_result,
-                             doc_store_->Put(parent_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument parent_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(parent_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult parent_put_result,
+      doc_store_->Put(parent_tokenized_document.document_wrapper()));
 
   // Create and put child document. Also tokenize it.
-  DocumentProto document =
+  DocumentProto child_document =
       DocumentBuilder()
           .SetKey("icing", "fake_type/1")
           .SetSchema(std::string(kFakeType))
           .AddStringProperty(std::string(kPropertyQualifiedId),
                              "pkg$db/ns#ref_type/1")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult child_put_result,
-                             doc_store_->Put(document));
   ICING_ASSERT_OK_AND_ASSIGN(
-      TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument child_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(child_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult child_put_result,
+      doc_store_->Put(child_tokenized_document.document_wrapper()));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
 
   // Handle document with document_id > last_added_document_id in recovery mode.
   // The handler should index this document and update last_added_document_id.
@@ -717,21 +855,22 @@
       child_put_result.new_document_id - 1);
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(child_put_result.new_document_id - 1));
-  EXPECT_THAT(
-      handler->Handle(tokenized_document, child_put_result.new_document_id,
-                      child_put_result.old_document_id, /*recovery_mode=*/true,
-                      /*put_document_stats=*/nullptr),
-      IsOk());
+  EXPECT_THAT(handler->Handle(
+                  child_tokenized_document, child_put_result.new_document_id,
+                  child_put_result.old_document_id, /*recovery_mode=*/true,
+                  /*put_document_stats=*/nullptr),
+              IsOk());
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(child_put_result.new_document_id));
   EXPECT_THAT(qualified_id_join_index_, Pointee(SizeIs(1)));
   EXPECT_THAT(
-      qualified_id_join_index_->Get(parent_put_result.new_document_id),
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+          parent_put_result.new_document_id),
       IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
           child_put_result.new_document_id, fake_type_joinable_property_id_))));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerV3Test,
+TEST_P(QualifiedIdJoinIndexingHandlerV3Test,
        HandleRecoveryModeShouldIgnoreDocsLeLastAddedDocId) {
   // Create and put parent document.
   DocumentProto parent_document =
@@ -740,28 +879,39 @@
           .SetSchema(std::string(kReferencedType))
           .AddStringProperty(std::string(kPropertyName), "one")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult parent_put_result,
-                             doc_store_->Put(parent_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument parent_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(parent_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult parent_put_result,
+      doc_store_->Put(parent_tokenized_document.document_wrapper()));
 
   // Create and put child document. Also tokenize it.
-  DocumentProto document =
+  DocumentProto child_document =
       DocumentBuilder()
           .SetKey("icing", "fake_type/1")
           .SetSchema(std::string(kFakeType))
           .AddStringProperty(std::string(kPropertyQualifiedId),
                              "pkg$db/ns#ref_type/1")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult child_put_result,
-                             doc_store_->Put(document));
   ICING_ASSERT_OK_AND_ASSIGN(
-      TokenizedDocument tokenized_document,
-      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                std::move(document)));
+      TokenizedDocument child_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(child_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult child_put_result,
+      doc_store_->Put(child_tokenized_document.document_wrapper()));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
-                                             qualified_id_join_index_.get()));
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
 
   // Handle document with document_id == last_added_document_id in recovery
   // mode. We should not get any error, but the handler should ignore the
@@ -771,15 +921,16 @@
       child_put_result.new_document_id);
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(child_put_result.new_document_id));
-  EXPECT_THAT(
-      handler->Handle(tokenized_document, child_put_result.new_document_id,
-                      child_put_result.old_document_id, /*recovery_mode=*/true,
-                      /*put_document_stats=*/nullptr),
-      IsOk());
+  EXPECT_THAT(handler->Handle(
+                  child_tokenized_document, child_put_result.new_document_id,
+                  child_put_result.old_document_id, /*recovery_mode=*/true,
+                  /*put_document_stats=*/nullptr),
+              IsOk());
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(child_put_result.new_document_id));
   EXPECT_THAT(qualified_id_join_index_, Pointee(IsEmpty()));
-  EXPECT_THAT(qualified_id_join_index_->Get(parent_put_result.new_document_id),
+  EXPECT_THAT(qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+                  parent_put_result.new_document_id),
               IsOkAndHolds(IsEmpty()));
 
   // Handle document with document_id < last_added_document_id in recovery mode.
@@ -789,18 +940,427 @@
       child_put_result.new_document_id + 1);
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(child_put_result.new_document_id + 1));
-  EXPECT_THAT(
-      handler->Handle(tokenized_document, child_put_result.new_document_id,
-                      child_put_result.old_document_id, /*recovery_mode=*/true,
-                      /*put_document_stats=*/nullptr),
-      IsOk());
+  EXPECT_THAT(handler->Handle(
+                  child_tokenized_document, child_put_result.new_document_id,
+                  child_put_result.old_document_id, /*recovery_mode=*/true,
+                  /*put_document_stats=*/nullptr),
+              IsOk());
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(child_put_result.new_document_id + 1));
   EXPECT_THAT(qualified_id_join_index_, Pointee(IsEmpty()));
-  EXPECT_THAT(qualified_id_join_index_->Get(parent_put_result.new_document_id),
+  EXPECT_THAT(qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+                  parent_put_result.new_document_id),
               IsOkAndHolds(IsEmpty()));
 }
 
+TEST_P(QualifiedIdJoinIndexingHandlerV3Test, HandleJoinWithMissingParent) {
+  if (!feature_flags_->enable_non_existent_qualified_id_join()) {
+    return;
+  }
+
+  // Create and index a child document that refers to a non-existent parent.
+  DocumentProto child_document =
+      DocumentBuilder()
+          .SetKey("icing", "fake_type/1")
+          .SetSchema(std::string(kFakeType))
+          .AddStringProperty(std::string(kPropertyQualifiedId),
+                             "pkg$db/ns#ref_type/1")  // This parent doesn't
+                                                      // exist yet
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument child_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(child_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult child_put_result,
+      doc_store_->Put(child_tokenized_document.document_wrapper()));
+
+  // Handle the child document.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
+      QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
+  EXPECT_THAT(handler->Handle(
+                  child_tokenized_document, child_put_result.new_document_id,
+                  child_put_result.old_document_id, /*recovery_mode=*/false,
+                  /*put_document_stats=*/nullptr),
+              IsOk());
+
+  // At this point, there's no parent document, so no entry in the doc id map.
+  // The qualified id map should have an entry, and the index size should be 1.
+  EXPECT_THAT(qualified_id_join_index_, Pointee(SizeIs(1)));
+
+  // Now, create and index the parent document.
+  DocumentProto parent_document =
+      DocumentBuilder()
+          .SetKey("pkg$db/ns", "ref_type/1")
+          .SetSchema(std::string(kReferencedType))
+          .AddStringProperty(std::string(kPropertyName), "one")
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument parent_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(parent_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult parent_put_result,
+      doc_store_->Put(parent_tokenized_document.document_wrapper()));
+
+  // Handle the parent document. This should trigger migration.
+  EXPECT_THAT(handler->Handle(
+                  parent_tokenized_document, parent_put_result.new_document_id,
+                  parent_put_result.old_document_id, /*recovery_mode=*/false,
+                  /*put_document_stats=*/nullptr),
+              IsOk());
+
+  // Verify that the join is now established.
+  EXPECT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+          parent_put_result.new_document_id),
+      IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
+          child_put_result.new_document_id, fake_type_joinable_property_id_))));
+
+  // The index size should still be 1.
+  EXPECT_THAT(qualified_id_join_index_, Pointee(SizeIs(1)));
+
+  // Update the parent document and index it again.
+  DocumentProto parent_document_replaced =
+      DocumentBuilder()
+          .SetKey("pkg$db/ns", "ref_type/1")
+          .SetSchema(std::string(kReferencedType))
+          .AddStringProperty(std::string(kPropertyName), "one_replaced")
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument parent_tokenized_document_replaced,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(parent_document_replaced)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult parent_put_result2,
+      doc_store_->Put(parent_tokenized_document_replaced.document_wrapper()));
+  ASSERT_THAT(parent_put_result2.old_document_id,
+              Eq(parent_put_result.new_document_id));
+
+  // Handle the updated parent.
+  EXPECT_THAT(
+      handler->Handle(parent_tokenized_document_replaced,
+                      parent_put_result2.new_document_id,
+                      parent_put_result2.old_document_id,
+                      /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
+      IsOk());
+
+  // Verify migration.
+  // Get() with old parent document id should return empty list.
+  EXPECT_THAT(qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+                  parent_put_result.new_document_id),
+              IsOkAndHolds(IsEmpty()));
+  // Get() with new parent document id should return child join id pair.
+  EXPECT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+          parent_put_result2.new_document_id),
+      IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
+          child_put_result.new_document_id, fake_type_joinable_property_id_))));
+  // Index size is still 1.
+  EXPECT_THAT(qualified_id_join_index_, Pointee(SizeIs(1)));
+}
+
+TEST_P(QualifiedIdJoinIndexingHandlerV3Test,
+       HandleJoinWithMultipleMissingParents) {
+  if (!feature_flags_->enable_non_existent_qualified_id_join()) {
+    return;
+  }
+
+  // Create and index a child document that refers to two non-existent parents.
+  DocumentProto child_document =
+      DocumentBuilder()
+          .SetKey("icing", "fake_type/1")
+          .SetSchema(std::string(kFakeType))
+          .AddStringProperty(std::string(kPropertyQualifiedId),
+                             "pkg$db/ns#ref_type/1", "pkg$db/ns#ref_type/2")
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument child_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(child_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult child_put_result,
+      doc_store_->Put(child_tokenized_document.document_wrapper()));
+
+  // Handle the child document.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
+      QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
+  EXPECT_THAT(handler->Handle(
+                  child_tokenized_document, child_put_result.new_document_id,
+                  child_put_result.old_document_id, /*recovery_mode=*/false,
+                  /*put_document_stats=*/nullptr),
+              IsOk());
+
+  // The qualified id map should have two entries, so the index size should be
+  // 2.
+  EXPECT_THAT(qualified_id_join_index_, Pointee(SizeIs(2)));
+
+  // Now, create and index the parent documents.
+  DocumentProto parent_document1 =
+      DocumentBuilder()
+          .SetKey("pkg$db/ns", "ref_type/1")
+          .SetSchema(std::string(kReferencedType))
+          .AddStringProperty(std::string(kPropertyName), "one")
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument parent_tokenized_document1,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(parent_document1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult parent_put_result1,
+      doc_store_->Put(parent_tokenized_document1.document_wrapper()));
+  DocumentProto parent_document2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/ns", "ref_type/2")
+          .SetSchema(std::string(kReferencedType))
+          .AddStringProperty(std::string(kPropertyName), "two")
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument parent_tokenized_document2,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(parent_document2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult parent_put_result2,
+      doc_store_->Put(parent_tokenized_document2.document_wrapper()));
+
+  // Handle the parent documents.
+  EXPECT_THAT(handler->Handle(parent_tokenized_document1,
+                              parent_put_result1.new_document_id,
+                              parent_put_result1.old_document_id,
+                              /*recovery_mode=*/false,
+                              /*put_document_stats=*/nullptr),
+              IsOk());
+  EXPECT_THAT(handler->Handle(parent_tokenized_document2,
+                              parent_put_result2.new_document_id,
+                              parent_put_result2.old_document_id,
+                              /*recovery_mode=*/false,
+                              /*put_document_stats=*/nullptr),
+              IsOk());
+
+  // Verify that the joins are now established for both parents.
+  EXPECT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+          parent_put_result1.new_document_id),
+      IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
+          child_put_result.new_document_id, fake_type_joinable_property_id_))));
+  EXPECT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+          parent_put_result2.new_document_id),
+      IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
+          child_put_result.new_document_id, fake_type_joinable_property_id_))));
+
+  // Index size should still be 2.
+  EXPECT_THAT(qualified_id_join_index_, Pointee(SizeIs(2)));
+}
+
+TEST_P(QualifiedIdJoinIndexingHandlerV3Test, HandleJoinWithMixedParents) {
+  if (!feature_flags_->enable_non_existent_qualified_id_join()) {
+    return;
+  }
+
+  // Create and index one parent document.
+  DocumentProto existing_parent =
+      DocumentBuilder()
+          .SetKey("pkg$db/ns", "ref_type/1")
+          .SetSchema(std::string(kReferencedType))
+          .AddStringProperty(std::string(kPropertyName), "one")
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument existing_parent_tokenized,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(existing_parent)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult existing_parent_put_result,
+      doc_store_->Put(existing_parent_tokenized.document_wrapper()));
+
+  // Create the handler.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
+      QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
+
+  // Create and index a child document that refers to the existing parent and
+  // a missing parent.
+  DocumentProto child_document =
+      DocumentBuilder()
+          .SetKey("icing", "fake_type/1")
+          .SetSchema(std::string(kFakeType))
+          .AddStringProperty(std::string(kPropertyQualifiedId),
+                             // existing
+                             "pkg$db/ns#ref_type/1",
+                             // missing
+                             "pkg$db/ns#ref_type/2")
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument child_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(child_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult child_put_result,
+      doc_store_->Put(child_tokenized_document.document_wrapper()));
+
+  // Handle the child document.
+  EXPECT_THAT(handler->Handle(
+                  child_tokenized_document, child_put_result.new_document_id,
+                  child_put_result.old_document_id, /*recovery_mode=*/false,
+                  /*put_document_stats=*/nullptr),
+              IsOk());
+
+  // Index size should be 2.
+  EXPECT_THAT(qualified_id_join_index_, Pointee(SizeIs(2)));
+
+  // Verify join for existing parent.
+  EXPECT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+          existing_parent_put_result.new_document_id),
+      IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
+          child_put_result.new_document_id, fake_type_joinable_property_id_))));
+
+  // Index the missing parent.
+  DocumentProto missing_parent =
+      DocumentBuilder()
+          .SetKey("pkg$db/ns", "ref_type/2")
+          .SetSchema(std::string(kReferencedType))
+          .AddStringProperty(std::string(kPropertyName), "two")
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument missing_parent_tokenized,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(missing_parent)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult missing_parent_put_result,
+      doc_store_->Put(missing_parent_tokenized.document_wrapper()));
+
+  // Handle the missing parent.
+  EXPECT_THAT(
+      handler->Handle(missing_parent_tokenized,
+                      missing_parent_put_result.new_document_id,
+                      missing_parent_put_result.old_document_id,
+                      /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
+      IsOk());
+
+  // Verify join for the now-existing parent.
+  EXPECT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+          missing_parent_put_result.new_document_id),
+      IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
+          child_put_result.new_document_id, fake_type_joinable_property_id_))));
+
+  // Verify join for the existing parent is still there.
+  EXPECT_THAT(
+      qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+          existing_parent_put_result.new_document_id),
+      IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
+          child_put_result.new_document_id, fake_type_joinable_property_id_))));
+
+  // Index size should still be 2.
+  EXPECT_THAT(qualified_id_join_index_, Pointee(SizeIs(2)));
+}
+
+TEST_P(QualifiedIdJoinIndexingHandlerV3Test,
+       HandleJoinWithMissingParent_FeatureDisabled) {
+  if (feature_flags_->enable_non_existent_qualified_id_join()) {
+    return;
+  }
+
+  // Create and index a child document that refers to a non-existent parent.
+  DocumentProto child_document =
+      DocumentBuilder()
+          .SetKey("icing", "fake_type/1")
+          .SetSchema(std::string(kFakeType))
+          .AddStringProperty(
+              std::string(kPropertyQualifiedId),
+              "pkg$db/ns#ref_type/1")  // This parent doesn't exist
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument child_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(child_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult child_put_result,
+      doc_store_->Put(child_tokenized_document.document_wrapper()));
+
+  // Create the handler with the feature flag disabled.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
+      QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
+                                             qualified_id_join_index_.get(),
+                                             feature_flags_.get()));
+
+  // Handle the child document.
+  EXPECT_THAT(handler->Handle(
+                  child_tokenized_document, child_put_result.new_document_id,
+                  child_put_result.old_document_id, /*recovery_mode=*/false,
+                  /*put_document_stats=*/nullptr),
+              IsOk());
+
+  // With the feature disabled, no join data should be indexed for a missing
+  // parent.
+  EXPECT_THAT(qualified_id_join_index_, Pointee(IsEmpty()));
+
+  // Now, create and index the parent document.
+  DocumentProto parent_document =
+      DocumentBuilder()
+          .SetKey("pkg$db/ns", "ref_type/1")
+          .SetSchema(std::string(kReferencedType))
+          .AddStringProperty(std::string(kPropertyName), "one")
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument parent_tokenized_document,
+      TokenizedDocument::Create(
+          schema_store_.get(), lang_segmenter_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          std::move(parent_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult parent_put_result,
+      doc_store_->Put(parent_tokenized_document.document_wrapper()));
+
+  // Handle the parent document.
+  EXPECT_THAT(handler->Handle(
+                  parent_tokenized_document, parent_put_result.new_document_id,
+                  parent_put_result.old_document_id, /*recovery_mode=*/false,
+                  /*put_document_stats=*/nullptr),
+              IsOk());
+
+  // Verify that no join has been established.
+  EXPECT_THAT(qualified_id_join_index_->GetDocumentJoinIdPairArrayView(
+                  parent_put_result.new_document_id),
+              IsOkAndHolds(IsEmpty()));
+
+  EXPECT_THAT(qualified_id_join_index_, Pointee(IsEmpty()));
+}
+
+INSTANTIATE_TEST_SUITE_P(QualifiedIdJoinIndexingHandlerV3Test,
+                         QualifiedIdJoinIndexingHandlerV3Test,
+                         // Parameter: enable_non_existent_qualified_id_join
+                         testing::Values(true, false));
 }  // namespace
 
 }  // namespace lib
diff --git a/icing/join/qualified-id-join-indexing-handler.cc b/icing/join/qualified-id-join-indexing-handler.cc
index b90227f..4061cae 100644
--- a/icing/join/qualified-id-join-indexing-handler.cc
+++ b/icing/join/qualified-id-join-indexing-handler.cc
@@ -25,6 +25,7 @@
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/canonical_errors.h"
+#include "icing/feature-flags.h"
 #include "icing/join/document-join-id-pair.h"
 #include "icing/join/qualified-id-join-index.h"
 #include "icing/join/qualified-id.h"
@@ -48,14 +49,16 @@
     std::unique_ptr<QualifiedIdJoinIndexingHandler>>
 QualifiedIdJoinIndexingHandler::Create(
     const Clock* clock, const DocumentStore* doc_store,
-    QualifiedIdJoinIndex* qualified_id_join_index) {
+    QualifiedIdJoinIndex* qualified_id_join_index,
+    const FeatureFlags* feature_flags) {
   ICING_RETURN_ERROR_IF_NULL(clock);
   ICING_RETURN_ERROR_IF_NULL(doc_store);
   ICING_RETURN_ERROR_IF_NULL(qualified_id_join_index);
+  ICING_RETURN_ERROR_IF_NULL(feature_flags);
 
   return std::unique_ptr<QualifiedIdJoinIndexingHandler>(
-      new QualifiedIdJoinIndexingHandler(clock, doc_store,
-                                         qualified_id_join_index));
+      new QualifiedIdJoinIndexingHandler(
+          clock, doc_store, qualified_id_join_index, feature_flags));
 }
 
 libtextclassifier3::Status QualifiedIdJoinIndexingHandler::Handle(
@@ -165,6 +168,16 @@
   if (IsDocumentIdValid(old_document_id)) {
     ICING_RETURN_IF_ERROR(
         qualified_id_join_index_.MigrateParent(old_document_id, document_id));
+  } else if (feature_flags_.enable_non_existent_qualified_id_join()) {
+    // This means that the document is new, which could also have child
+    // documents since child documents are allowed to reference non-existing
+    // parent documents. Now, we try to migrate its child documents to the new
+    // document id.
+    const DocumentProto& document =
+        tokenized_document.document_wrapper().document();
+    QualifiedId qualified_id_str(document.namespace_(), document.uri());
+    ICING_RETURN_IF_ERROR(
+        qualified_id_join_index_.MigrateParent(qualified_id_str, document_id));
   }
 
   // (Child perspective)
@@ -178,9 +191,14 @@
     DocumentJoinIdPair child_doc_join_id_pair(
         document_id, qualified_id_property.metadata.id);
 
-    // Extract parent qualified ids and lookup their corresponding document ids.
-    std::vector<DocumentId> parent_doc_ids;
-    parent_doc_ids.reserve(qualified_id_property.values.size());
+    // Extract parent qualified ids.
+    std::vector<DocumentId> parent_doc_ids;  // Deprecated.
+    std::vector<QualifiedId> parent_qualified_ids;
+    if (feature_flags_.enable_non_existent_qualified_id_join()) {
+      parent_qualified_ids.reserve(qualified_id_property.values.size());
+    } else {
+      parent_doc_ids.reserve(qualified_id_property.values.size());
+    }
     for (std::string_view parent_qualified_id_str :
          qualified_id_property.values) {
       libtextclassifier3::StatusOr<QualifiedId> parent_qualified_id_or =
@@ -191,6 +209,10 @@
       }
       QualifiedId parent_qualified_id =
           std::move(parent_qualified_id_or).ValueOrDie();
+      if (feature_flags_.enable_non_existent_qualified_id_join()) {
+        parent_qualified_ids.push_back(std::move(parent_qualified_id));
+        continue;
+      }
 
       // Lookup document store to get the parent document id.
       libtextclassifier3::StatusOr<DocumentId> parent_doc_id_or =
@@ -203,10 +225,17 @@
       }
       parent_doc_ids.push_back(parent_doc_id_or.ValueOrDie());
     }
+    libtextclassifier3::Status status;
+    if (feature_flags_.enable_non_existent_qualified_id_join()) {
+      // Index join relationship by parent qualified ids.
+      status = qualified_id_join_index_.Put(&doc_store_, child_doc_join_id_pair,
+                                            std::move(parent_qualified_ids));
 
-    // Add all parent document ids to the index.
-    libtextclassifier3::Status status = qualified_id_join_index_.Put(
-        child_doc_join_id_pair, std::move(parent_doc_ids));
+    } else {
+      // Index join relationship by parent document ids.
+      status = qualified_id_join_index_.Put(child_doc_join_id_pair,
+                                            std::move(parent_doc_ids));
+    }
     if (!status.ok()) {
       ICING_LOG(WARNING)
           << "Failed to add data into qualified id join index due to: "
diff --git a/icing/join/qualified-id-join-indexing-handler.h b/icing/join/qualified-id-join-indexing-handler.h
index f321f4d..9abf249 100644
--- a/icing/join/qualified-id-join-indexing-handler.h
+++ b/icing/join/qualified-id-join-indexing-handler.h
@@ -19,6 +19,7 @@
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/feature-flags.h"
 #include "icing/index/data-indexing-handler.h"
 #include "icing/join/qualified-id-join-index.h"
 #include "icing/proto/logging.pb.h"
@@ -42,7 +43,8 @@
   static libtextclassifier3::StatusOr<
       std::unique_ptr<QualifiedIdJoinIndexingHandler>>
   Create(const Clock* clock, const DocumentStore* doc_store,
-         QualifiedIdJoinIndex* qualified_id_join_index);
+         QualifiedIdJoinIndex* qualified_id_join_index,
+         const FeatureFlags* feature_flags);
 
   ~QualifiedIdJoinIndexingHandler() override = default;
 
@@ -72,10 +74,12 @@
  private:
   explicit QualifiedIdJoinIndexingHandler(
       const Clock* clock, const DocumentStore* doc_store,
-      QualifiedIdJoinIndex* qualified_id_join_index)
+      QualifiedIdJoinIndex* qualified_id_join_index,
+      const FeatureFlags* feature_flags)
       : DataIndexingHandler(clock),
         doc_store_(*doc_store),
-        qualified_id_join_index_(*qualified_id_join_index) {}
+        qualified_id_join_index_(*qualified_id_join_index),
+        feature_flags_(*feature_flags) {}
 
   // TODO(b/275121148): deprecate v2 after rollout v3.
 
@@ -90,6 +94,7 @@
 
   const DocumentStore& doc_store_;                 // Does not own.
   QualifiedIdJoinIndex& qualified_id_join_index_;  // Does not own.
+  const FeatureFlags& feature_flags_;              // Does not own.
 };
 
 }  // namespace lib
diff --git a/icing/join/qualified-id.cc b/icing/join/qualified-id.cc
index 42e080c..370bbcb 100644
--- a/icing/join/qualified-id.cc
+++ b/icing/join/qualified-id.cc
@@ -14,8 +14,10 @@
 
 #include "icing/join/qualified-id.h"
 
+#include <cstddef>
 #include <string>
 #include <string_view>
+#include <utility>
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/canonical_errors.h"
@@ -33,6 +35,19 @@
          c == QualifiedId::kNamespaceUriSeparator;
 }
 
+// Helper function to escape the content.
+std::string Escape(std::string_view content) {
+  std::string escaped_content;
+  escaped_content.reserve(content.length());
+  for (char c : content) {
+    if (IsSpecialCharacter(c)) {
+      escaped_content += QualifiedId::kEscapeChar;
+    }
+    escaped_content += c;
+  }
+  return escaped_content;
+}
+
 // Helper function to verify the format (check the escape format and make sure
 // number of separator '#' is 1) and find the position of the unique separator.
 //
@@ -106,5 +121,9 @@
   return QualifiedId(std::move(name_space), std::move(uri));
 }
 
+std::string QualifiedId::ToString() const {
+  return Escape(name_space_) + kNamespaceUriSeparator + Escape(uri_);
+}
+
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/join/qualified-id.h b/icing/join/qualified-id.h
index eb6606a..d3e8d39 100644
--- a/icing/join/qualified-id.h
+++ b/icing/join/qualified-id.h
@@ -15,8 +15,11 @@
 #ifndef ICING_JOIN_QUALIFIED_ID_H_
 #define ICING_JOIN_QUALIFIED_ID_H_
 
+#include <cstddef>
+#include <functional>
 #include <string>
 #include <string_view>
+#include <utility>
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 
@@ -34,6 +37,13 @@
 // - Raw namespace and uri cannot be empty.
 class QualifiedId {
  public:
+  struct Hasher {
+    std::size_t operator()(const QualifiedId& qualified_id) const {
+      return std::hash<std::string>()(qualified_id.name_space_) ^
+             std::hash<std::string>()(qualified_id.uri_);
+    }
+  };
+
   static constexpr char kEscapeChar = '\\';
   static constexpr char kNamespaceUriSeparator = '#';
 
@@ -51,9 +61,24 @@
   explicit QualifiedId(std::string name_space, std::string uri)
       : name_space_(std::move(name_space)), uri_(std::move(uri)) {}
 
+  bool operator==(const QualifiedId& other) const {
+    return name_space_ == other.name_space_ && uri_ == other.uri_;
+  }
+
+  bool operator<(const QualifiedId& other) const {
+    if (name_space_ != other.name_space_) {
+      return name_space_ < other.name_space_;
+    }
+    return uri_ < other.uri_;
+  }
+
   const std::string& name_space() const { return name_space_; }
   const std::string& uri() const { return uri_; }
 
+  // Unparses the QualifiedId to its string representation:
+  // "<escaped(namespace)>#<escaped(uri)>".
+  std::string ToString() const;
+
  private:
   std::string name_space_;
   std::string uri_;
diff --git a/icing/join/qualified-id_test.cc b/icing/join/qualified-id_test.cc
index 92bf63e..59e9e51 100644
--- a/icing/join/qualified-id_test.cc
+++ b/icing/join/qualified-id_test.cc
@@ -153,6 +153,43 @@
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
+TEST(QualifiedIdTest, ToStringAndParse) {
+  // "namespace" + "uri" -> "namespace#uri"
+  QualifiedId id1("namespace", "uri");
+  std::string str1 = id1.ToString();
+  EXPECT_THAT(str1, Eq("namespace#uri"));
+  ICING_ASSERT_OK_AND_ASSIGN(QualifiedId parsed_id1, QualifiedId::Parse(str1));
+  EXPECT_THAT(parsed_id1, Eq(id1));
+
+  // "namespace\" + "uri" -> "namespace\\#uri"
+  QualifiedId id2("namespace\\", "uri");
+  std::string str2 = id2.ToString();
+  EXPECT_THAT(str2, Eq("namespace\\\\#uri"));
+  ICING_ASSERT_OK_AND_ASSIGN(QualifiedId parsed_id2, QualifiedId::Parse(str2));
+  EXPECT_THAT(parsed_id2, Eq(id2));
+
+  // "namespace#" + "uri" -> "namespace\##uri"
+  QualifiedId id3("namespace#", "uri");
+  std::string str3 = id3.ToString();
+  EXPECT_THAT(str3, Eq("namespace\\##uri"));
+  ICING_ASSERT_OK_AND_ASSIGN(QualifiedId parsed_id3, QualifiedId::Parse(str3));
+  EXPECT_THAT(parsed_id3, Eq(id3));
+
+  // "namespace" + "#uri" -> "namespace#\#uri"
+  QualifiedId id4("namespace", "#uri");
+  std::string str4 = id4.ToString();
+  EXPECT_THAT(str4, Eq("namespace#\\#uri"));
+  ICING_ASSERT_OK_AND_ASSIGN(QualifiedId parsed_id4, QualifiedId::Parse(str4));
+  EXPECT_THAT(parsed_id4, Eq(id4));
+
+  // "namespace\#" + "#\uri" -> "namespace\\\##\#\\uri"
+  QualifiedId id5("namespace\\#", "#\\uri");
+  std::string str5 = id5.ToString();
+  EXPECT_THAT(str5, Eq("namespace\\\\\\##\\#\\\\uri"));
+  ICING_ASSERT_OK_AND_ASSIGN(QualifiedId parsed_id5, QualifiedId::Parse(str5));
+  EXPECT_THAT(parsed_id5, Eq(id5));
+}
+
 }  // namespace
 
 }  // namespace lib
diff --git a/icing/legacy/index/icing-array-storage.h b/icing/legacy/index/icing-array-storage.h
index 6932f8b..88dc59f 100644
--- a/icing/legacy/index/icing-array-storage.h
+++ b/icing/legacy/index/icing-array-storage.h
@@ -20,10 +20,13 @@
 #ifndef ICING_LEGACY_INDEX_ICING_ARRAY_STORAGE_H_
 #define ICING_LEGACY_INDEX_ICING_ARRAY_STORAGE_H_
 
+#include <cstddef>
 #include <cstdint>
 #include <string>
 #include <vector>
 
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/absl_ports/canonical_errors.h"
 #include "icing/legacy/index/icing-filesystem.h"
 #include "icing/legacy/index/icing-mmapper.h"
 #include "icing/util/crc32.h"
@@ -71,11 +74,19 @@
   // Make array empty again.
   void Clear();
 
-  // Intent to write memory at (elt_idx, elt_idx + elt_len). Returns
-  // NULL if file cannot be grown to accommodate that offset.
+  // Intent to write memory at (elt_idx, elt_idx + elt_len).
+  //
+  // Returns:
+  //   The pointer to the memory on success.
+  //   RESOURCE_EXHAUSTED if file cannot be grown to accommodate that offset.
   template <class T>
-  T *GetMutableMem(uint32_t elt_idx, uint32_t elt_len) {
-    return static_cast<T *>(GetMutableMemInternal(elt_idx, elt_len));
+  libtextclassifier3::StatusOr<T *> GetMutableMem(uint32_t elt_idx,
+                                                  uint32_t elt_len) {
+    void *mem = GetMutableMemInternal(elt_idx, elt_len);
+    if (mem == nullptr) {
+      return absl_ports::ResourceExhaustedError("Failed to allocate memory");
+    }
+    return static_cast<T *>(mem);
   }
 
   // Resizes to first elt_len elements.
@@ -134,6 +145,7 @@
   static_assert(8 == sizeof(Change), "sizeof(Change) != 8");
   static_assert(4 == alignof(Change), "alignof(Change) != 4");
 
+  // Returns a pointer to the memory. Returns nullptr on failure.
   void *GetMutableMemInternal(uint32_t elt_idx, uint32_t elt_len);
 
   bool GrowIfNecessary(uint32_t num_elts);
diff --git a/icing/legacy/index/icing-array-storage_test.cc b/icing/legacy/index/icing-array-storage_test.cc
index 690c356..37c3f09 100644
--- a/icing/legacy/index/icing-array-storage_test.cc
+++ b/icing/legacy/index/icing-array-storage_test.cc
@@ -20,6 +20,7 @@
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "icing/legacy/index/icing-filesystem.h"
+#include "icing/testing/common-matchers.h"
 #include "icing/testing/tmp-directory.h"
 #include "icing/util/crc32.h"
 
@@ -61,7 +62,8 @@
   EXPECT_THAT(storage.UpdateCrc(), Eq(Crc32()));
   EXPECT_THAT(storage.GetCrc(), Eq(Crc32()));
 
-  uint32_t* val = storage.GetMutableMem<uint32_t>(0, 1);
+  ICING_ASSERT_OK_AND_ASSIGN(uint32_t* val,
+                             storage.GetMutableMem<uint32_t>(0, 1));
   *val = 5;
 
   // Because there is no crc_ptr, the crc should remain 0 even though we have
@@ -85,7 +87,8 @@
   EXPECT_THAT(storage.UpdateCrc(), Eq(Crc32()));
   EXPECT_THAT(storage.GetCrc(), Eq(Crc32()));
 
-  uint32_t* val = storage.GetMutableMem<uint32_t>(0, 1);
+  ICING_ASSERT_OK_AND_ASSIGN(uint32_t* val,
+                             storage.GetMutableMem<uint32_t>(0, 1));
   *val = 5;
 
   EXPECT_THAT(storage.GetCrc(), Eq(Crc32(937357362)));
@@ -110,7 +113,8 @@
     EXPECT_THAT(storage_one.UpdateCrc(), Eq(Crc32()));
     EXPECT_THAT(storage_one.GetCrc(), Eq(Crc32()));
 
-    uint32_t* val = storage_one.GetMutableMem<uint32_t>(0, 1);
+    ICING_ASSERT_OK_AND_ASSIGN(uint32_t* val,
+                               storage_one.GetMutableMem<uint32_t>(0, 1));
     *val = 5;
 
     EXPECT_THAT(storage_one.GetCrc(), Eq(Crc32(937357362)));
@@ -144,7 +148,8 @@
     EXPECT_THAT(storage_one.UpdateCrc(), Eq(Crc32()));
     EXPECT_THAT(storage_one.GetCrc(), Eq(Crc32()));
 
-    uint32_t* val = storage_one.GetMutableMem<uint32_t>(0, 1);
+    ICING_ASSERT_OK_AND_ASSIGN(uint32_t* val,
+                               storage_one.GetMutableMem<uint32_t>(0, 1));
     *val = 5;
 
     EXPECT_THAT(storage_one.GetCrc(), Eq(Crc32(937357362)));
@@ -167,4 +172,4 @@
 }  // namespace
 
 }  // namespace lib
-}  // namespace icing
\ No newline at end of file
+}  // namespace icing
diff --git a/icing/legacy/index/icing-dynamic-trie.cc b/icing/legacy/index/icing-dynamic-trie.cc
index e665272..f385c94 100644
--- a/icing/legacy/index/icing-dynamic-trie.cc
+++ b/icing/legacy/index/icing-dynamic-trie.cc
@@ -71,6 +71,7 @@
 #include <cerrno>
 #include <cinttypes>
 #include <cstdint>
+#include <cstdlib>
 #include <cstring>
 #include <memory>
 #include <ostream>
@@ -80,6 +81,7 @@
 #include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/legacy/core/icing-string-util.h"
 #include "icing/legacy/core/icing-timer.h"
@@ -87,12 +89,15 @@
 #include "icing/legacy/index/icing-filesystem.h"
 #include "icing/legacy/index/icing-flash-bitmap.h"
 #include "icing/legacy/index/icing-mmapper.h"
+#include "icing/legacy/index/icing-storage.h"
 #include "icing/legacy/index/proto/icing-dynamic-trie-header.pb.h"
 #include "icing/util/crc32.h"
 #include "icing/util/i18n-utils.h"
 #include "icing/util/logging.h"
 #include "icing/util/math-util.h"
 #include "icing/util/status-macros.h"
+#include "unicode/utf8.h"
+#include "unicode/utypes.h"
 
 namespace icing {
 namespace lib {
@@ -285,6 +290,9 @@
     return &array_storage_[NODE].array_cast<Node>()[idx];
   }
 
+  // Returns the number of elements in the node array.
+  uint32_t GetNodeArraySize() const { return array_storage_[NODE].num_elts(); }
+
   // REQUIRES: !empty(). Otherwise node 0 could contain invalid data
   //   (next_index, is_leaf, log2_num_children).
   const Node *GetRootNode() const { return GetNode(0); }
@@ -308,9 +316,11 @@
   // returns a writable element or array within and sets
   // dirty_pages_[array_type] as a side effect, assuming the mutable
   // area will get written to.
-  Node *GetMutableNode(uint32_t idx);
-  Next *GetMutableNextArray(uint32_t idx, uint32_t len);
-  char *GetMutableSuffix(uint32_t idx, uint32_t len);
+  libtextclassifier3::StatusOr<Node *> GetMutableNode(uint32_t idx);
+  libtextclassifier3::StatusOr<Next *> GetMutableNextArray(uint32_t idx,
+                                                           uint32_t len);
+  libtextclassifier3::StatusOr<char *> GetMutableSuffix(uint32_t idx,
+                                                        uint32_t len);
 
   // Update crcs based on current contents. Returns all_crc or kNoCrc.
   Crc32 UpdateCrc();
@@ -324,13 +334,14 @@
   uint32_t suffixes_left() const;
 
   // REQUIRES: nodes_left() > 0.
-  Node *AllocNode();
+  libtextclassifier3::StatusOr<Node *> AllocNode();
   // REQUIRES: nexts_left() >= kMaxNextArraySize.
   libtextclassifier3::StatusOr<Next *> AllocNextArray(int size);
   void FreeNextArray(Next *next, int log2_size);
   // REQUIRES: suffixes_left() >= strlen(suffix) + 1 + value_size()
-  uint32_t MakeSuffix(std::string_view suffix, const void *value,
-                      uint32_t *value_index);
+  libtextclassifier3::StatusOr<uint32_t> MakeSuffix(std::string_view suffix,
+                                                    const void *value,
+                                                    uint32_t *value_index);
 
   const IcingDynamicTrieHeader &hdr() const { return hdr_.hdr; }
 
@@ -535,7 +546,7 @@
   if (!hdr_.Init(hdr_mmapper_.address(),
                  IcingMMapper::system_page_size() - sizeof(Crcs)) ||
       !hdr_.Verify()) {
-    ICING_LOG(ERROR) << "Trie reading header failed";
+    ICING_LOG(ERROR) << "Trie reading header failed. Path: " << file_basename_;
     goto failed;
   }
 
@@ -757,9 +768,10 @@
   return total;
 }
 
-IcingDynamicTrie::Node *IcingDynamicTrie::IcingDynamicTrieStorage::AllocNode() {
+libtextclassifier3::StatusOr<IcingDynamicTrie::Node *>
+IcingDynamicTrie::IcingDynamicTrieStorage::AllocNode() {
   if (nodes_left() == 0) {
-    ICING_LOG(FATAL) << "No allocated nodes left";
+    return absl_ports::ResourceExhaustedError("No allocated nodes left");
   }
 
   hdr_.hdr.set_num_nodes(hdr_.hdr.num_nodes() + 1);
@@ -774,7 +786,7 @@
   }
 
   if (nexts_left() < static_cast<uint32_t>(kMaxNextArraySize)) {
-    ICING_LOG(FATAL) << "'next' buffer not enough";
+    return absl_ports::ResourceExhaustedError("'next' buffer not enough");
   }
 
   // Compute ceil(log2(size)).
@@ -786,15 +798,17 @@
   // Look in free list.
   Next *ret;
   if (hdr_.hdr.free_lists(log2_size) != kInvalidNextIndex) {
-    ret = GetMutableNextArray(hdr_.hdr.free_lists(log2_size), aligned_size);
+    ICING_ASSIGN_OR_RETURN(
+        ret, GetMutableNextArray(hdr_.hdr.free_lists(log2_size), aligned_size));
     uint32_t next_link = ret->next_index();
     if (next_link != kInvalidNextIndex && next_link >= hdr_.hdr.max_nexts()) {
-      ICING_LOG(FATAL) << "'next' index is out of range";
+      return absl_ports::InternalError("'next' index is out of range");
     }
     hdr_.hdr.set_free_lists(log2_size, next_link);
   } else {
     // Allocate a new one.
-    ret = GetMutableNextArray(hdr_.hdr.num_nexts(), aligned_size);
+    ICING_ASSIGN_OR_RETURN(
+        ret, GetMutableNextArray(hdr_.hdr.num_nexts(), aligned_size));
     hdr_.hdr.set_num_nexts(hdr_.hdr.num_nexts() + aligned_size);
   }
 
@@ -816,14 +830,17 @@
   hdr_.hdr.set_free_lists(log2_size, GetNextArrayIndex(next));
 }
 
-uint32_t IcingDynamicTrie::IcingDynamicTrieStorage::MakeSuffix(
-    std::string_view suffix, const void *value, uint32_t *value_index) {
+libtextclassifier3::StatusOr<uint32_t>
+IcingDynamicTrie::IcingDynamicTrieStorage::MakeSuffix(std::string_view suffix,
+                                                      const void *value,
+                                                      uint32_t *value_index) {
   if (suffixes_left() < suffix.size() + 1 + value_size()) {
-    ICING_LOG(FATAL) << "'suffix' buffer not enough";
+    return absl_ports::ResourceExhaustedError("'suffix' buffer not enough");
   }
 
-  char *start = GetMutableSuffix(hdr_.hdr.suffixes_size(),
-                                 suffix.size() + 1 + value_size());
+  ICING_ASSIGN_OR_RETURN(char *start,
+                         GetMutableSuffix(hdr_.hdr.suffixes_size(),
+                                          suffix.size() + 1 + value_size()));
   // Copy suffix.
   memcpy(start, suffix.data(), suffix.size());
   // Set a '\0' after suffix.
@@ -908,19 +925,20 @@
   return hdr_.SerializeToArray(hdr_mmapper_.address(), serialized_header_max());
 }
 
-IcingDynamicTrie::Node *
+libtextclassifier3::StatusOr<IcingDynamicTrie::Node *>
 IcingDynamicTrie::IcingDynamicTrieStorage::GetMutableNode(uint32_t idx) {
   return array_storage_[NODE].GetMutableMem<Node>(idx, 1);
 }
 
-IcingDynamicTrie::Next *
+libtextclassifier3::StatusOr<IcingDynamicTrie::Next *>
 IcingDynamicTrie::IcingDynamicTrieStorage::GetMutableNextArray(uint32_t idx,
                                                                uint32_t len) {
   return array_storage_[NEXT].GetMutableMem<Next>(idx, len);
 }
 
-char *IcingDynamicTrie::IcingDynamicTrieStorage::GetMutableSuffix(
-    uint32_t idx, uint32_t len) {
+libtextclassifier3::StatusOr<char *>
+IcingDynamicTrie::IcingDynamicTrieStorage::GetMutableSuffix(uint32_t idx,
+                                                            uint32_t len) {
   return array_storage_[SUFFIX].GetMutableMem<char>(idx, len);
 }
 
@@ -958,7 +976,8 @@
   uint32_t magic;
   memcpy(&magic, buf, sizeof(magic));
   if (magic != kMagic) {
-    ICING_LOG(ERROR) << "Trie header magic mismatch";
+    ICING_LOG(ERROR) << "Invalid header magic for IcingDynamicTrie. Expected: "
+                     << kMagic << ", actual: " << magic;
     return false;
   }
   uint32_t len;
@@ -1558,49 +1577,42 @@
   deleted_bitmap_->Truncate(0);
 }
 
-bool IcingDynamicTrie::ClearSuffixAndValue(uint32_t suffix_value_index) {
+libtextclassifier3::Status IcingDynamicTrie::ClearSuffixAndValue(
+    uint32_t suffix_value_index) {
   // The size 1 below is for a '\0' between the suffix and the value.
   size_t suffix_and_value_length =
       strlen(this->storage_->GetSuffix(suffix_value_index)) + 1 +
       this->value_size();
-  char *mutable_suffix_and_value = this->storage_->GetMutableSuffix(
-      suffix_value_index, suffix_and_value_length);
-
-  if (mutable_suffix_and_value == nullptr) {
-    return false;
-  }
+  ICING_ASSIGN_OR_RETURN(char *mutable_suffix_and_value,
+                         this->storage_->GetMutableSuffix(
+                             suffix_value_index, suffix_and_value_length));
 
   memset(mutable_suffix_and_value, 0, suffix_and_value_length);
-  return true;
+  return libtextclassifier3::Status::OK;
 }
 
-bool IcingDynamicTrie::ResetNext(uint32_t next_index) {
-  Next *mutable_next =
-      this->storage_->GetMutableNextArray(next_index, /*len=*/1);
+libtextclassifier3::Status IcingDynamicTrie::ResetNext(uint32_t next_index) {
+  ICING_ASSIGN_OR_RETURN(
+      Next * mutable_next,
+      this->storage_->GetMutableNextArray(next_index, /*len=*/1));
 
-  if (mutable_next == nullptr) {
-    return false;
-  }
   ResetMutableNext(*mutable_next);
-  return true;
+  return libtextclassifier3::Status::OK;
 }
 
-bool IcingDynamicTrie::SortNextArray(const Node *node) {
+libtextclassifier3::Status IcingDynamicTrie::SortNextArray(const Node *node) {
   if (node == nullptr) {
     // Nothing to sort, return success directly.
-    return true;
+    return libtextclassifier3::Status::OK;
   }
 
   uint32_t next_array_buffer_size = 1u << node->log2_num_children();
-  Next *next_array_start = this->storage_->GetMutableNextArray(
-      node->next_index(), next_array_buffer_size);
-
-  if (next_array_start == nullptr) {
-    return false;
-  }
+  ICING_ASSIGN_OR_RETURN(Next * next_array_start,
+                         this->storage_->GetMutableNextArray(
+                             node->next_index(), next_array_buffer_size));
 
   std::sort(next_array_start, next_array_start + next_array_buffer_size);
-  return true;
+  return libtextclassifier3::Status::OK;
 }
 
 libtextclassifier3::Status IcingDynamicTrie::Insert(std::string_view key,
@@ -1609,7 +1621,7 @@
                                                     bool replace,
                                                     bool *pnew_key) {
   if (!is_initialized()) {
-    ICING_LOG(FATAL) << "DynamicTrie not initialized";
+    return absl_ports::FailedPreconditionError("DynamicTrie not initialized");
   }
 
   if (pnew_key) *pnew_key = false;
@@ -1632,22 +1644,27 @@
 
   uint32_t best_node_index;
   int key_offset;
-  FindBestNode(key, &best_node_index, &key_offset, false);
+  libtextclassifier3::Status status =
+      FindBestNode(key, &best_node_index, &key_offset, false);
 
   // A negative key_offset indicates that storage_ is empty
-  if (key_offset < 0) {
+  if (key_offset < 0 || !status.ok()) {
     // First key.
     if (!storage_->empty()) {
-      ICING_LOG(FATAL) << "Key offset is negative but storage is not empty, "
-                          "there're inconsistencies in dynamic trie.";
+      return absl_ports::InternalError(
+          "Key offset is negative but storage is not empty, "
+          "there're inconsistencies in dynamic trie.");
     }
-    Node *node = storage_->AllocNode();
-    node->set_next_index(storage_->MakeSuffix(key, value, value_index));
+    ICING_ASSIGN_OR_RETURN(Node * node, storage_->AllocNode());
+    ICING_ASSIGN_OR_RETURN(uint32_t next_index,
+                           storage_->MakeSuffix(key, value, value_index));
+    node->set_next_index(next_index);
     node->set_is_leaf(true);
     node->set_log2_num_children(0);
   } else if (storage_->GetNode(best_node_index)->is_leaf()) {
     // Prefix in the trie. Split at leaf.
-    Node *split_node = storage_->GetMutableNode(best_node_index);
+    ICING_ASSIGN_OR_RETURN(Node * split_node,
+                           storage_->GetMutableNode(best_node_index));
     const char *prev_suffix = storage_->GetSuffix(split_node->next_index());
 
     // Find the common prefix length starting from prev_suffix[0] and
@@ -1672,9 +1689,11 @@
       }
       // Update value if replace == true and return.
       if (replace) {
-        char *mutable_prev_suffix_cur = storage_->GetMutableSuffix(
-            storage_->GetSuffixIndex(prev_suffix + common_prefix_len + 1),
-            value_size());
+        ICING_ASSIGN_OR_RETURN(
+            char *mutable_prev_suffix_cur,
+            storage_->GetMutableSuffix(
+                storage_->GetSuffixIndex(prev_suffix + common_prefix_len + 1),
+                value_size()));
         memcpy(mutable_prev_suffix_cur, value, value_size());
       }
       return libtextclassifier3::Status::OK;
@@ -1689,7 +1708,7 @@
       split_node->set_next_index(storage_->GetNextArrayIndex(split_next));
       split_node->set_is_leaf(false);
       split_node->set_log2_num_children(0);
-      Node *child_node = storage_->AllocNode();
+      ICING_ASSIGN_OR_RETURN(Node * child_node, storage_->AllocNode());
       split_next[0].set_val(*(prev_suffix + i));
       split_next[0].set_node_index(storage_->GetNodeIndex(child_node));
 
@@ -1701,8 +1720,8 @@
     split_node->set_next_index(storage_->GetNextArrayIndex(split_next));
     split_node->set_is_leaf(false);
     split_node->set_log2_num_children(1);
-    Node *prev_suffix_node = storage_->AllocNode();
-    Node *key_node = storage_->AllocNode();
+    ICING_ASSIGN_OR_RETURN(Node * prev_suffix_node, storage_->AllocNode());
+    ICING_ASSIGN_OR_RETURN(Node * key_node, storage_->AllocNode());
     split_next[0].set_val(*(prev_suffix + common_prefix_len));
     split_next[0].set_node_index(storage_->GetNodeIndex(prev_suffix_node));
     if (*(prev_suffix + common_prefix_len)) {
@@ -1721,12 +1740,16 @@
     split_next[1].set_val(next_val);
     split_next[1].set_node_index(storage_->GetNodeIndex(key_node));
     if (next_val != '\0') {
-      uint32_t next_index = storage_->MakeSuffix(
-          key.substr(key_offset + common_prefix_len + 1), value, value_index);
+      ICING_ASSIGN_OR_RETURN(
+          uint32_t next_index,
+          storage_->MakeSuffix(key.substr(key_offset + common_prefix_len + 1),
+                               value, value_index));
       key_node->set_next_index(next_index);
     } else {
-      uint32_t next_index = storage_->MakeSuffix(
-          key.substr(key_offset + common_prefix_len), value, value_index);
+      ICING_ASSIGN_OR_RETURN(
+          uint32_t next_index,
+          storage_->MakeSuffix(key.substr(key_offset + common_prefix_len),
+                               value, value_index));
       key_node->set_next_index(next_index);
     }
     key_node->set_is_leaf(true);
@@ -1738,14 +1761,16 @@
     const Node *best_node = storage_->GetNode(best_node_index);
 
     // Add our value as a node + suffix.
-    Node *new_leaf_node = storage_->AllocNode();
+    ICING_ASSIGN_OR_RETURN(Node * new_leaf_node, storage_->AllocNode());
     if (key_offset < key.size()) {
-      uint32_t next_index =
-          storage_->MakeSuffix(key.substr(key_offset + 1), value, value_index);
+      ICING_ASSIGN_OR_RETURN(
+          uint32_t next_index,
+          storage_->MakeSuffix(key.substr(key_offset + 1), value, value_index));
       new_leaf_node->set_next_index(next_index);
     } else {
-      uint32_t next_index =
-          storage_->MakeSuffix(key.substr(key_offset), value, value_index);
+      ICING_ASSIGN_OR_RETURN(
+          uint32_t next_index,
+          storage_->MakeSuffix(key.substr(key_offset), value, value_index));
       new_leaf_node->set_next_index(next_index);
     }
     new_leaf_node->set_is_leaf(true);
@@ -1753,8 +1778,9 @@
 
     // Figure out the real length of the existing next array.
     uint32_t next_array_buffer_size = 1u << best_node->log2_num_children();
-    Next *cur_next = storage_->GetMutableNextArray(best_node->next_index(),
-                                                   next_array_buffer_size);
+    ICING_ASSIGN_OR_RETURN(
+        Next * cur_next, storage_->GetMutableNextArray(best_node->next_index(),
+                                                       next_array_buffer_size));
     int next_len = GetValidNextsSize(cur_next, next_array_buffer_size);
     Next *new_next = cur_next;
     if (next_len == (next_array_buffer_size)) {
@@ -1772,8 +1798,9 @@
     // If this was new, update the parent node and free the old next
     // array.
     if (new_next != cur_next) {
-      Node *mutable_best_node =
-          storage_->GetMutableNode(storage_->GetNodeIndex(best_node));
+      ICING_ASSIGN_OR_RETURN(
+          Node * mutable_best_node,
+          storage_->GetMutableNode(storage_->GetNodeIndex(best_node)));
       mutable_best_node->set_next_index(storage_->GetNextArrayIndex(new_next));
       mutable_best_node->set_is_leaf(false);
       uint8_t log2_num_children = mutable_best_node->log2_num_children();
@@ -1806,18 +1833,21 @@
   return static_cast<const void *>(storage_->GetSuffix(value_index));
 }
 
-void IcingDynamicTrie::SetValueAtIndex(uint32_t value_index,
-                                       const void *value) {
+libtextclassifier3::Status IcingDynamicTrie::SetValueAtIndex(
+    uint32_t value_index, const void *value) {
   if (!is_initialized()) {
-    ICING_LOG(FATAL) << "DynamicTrie not initialized";
+    return absl_ports::FailedPreconditionError("DynamicTrie not initialized");
   }
 
   if (value_index > storage_->hdr().max_suffixes_size() - value_size()) {
-    ICING_LOG(FATAL) << "Value index is out of range";
+    return absl_ports::OutOfRangeError("Value index is out of range");
   }
 
-  memcpy(storage_->GetMutableSuffix(value_index, value_size()), value,
-         value_size());
+  ICING_ASSIGN_OR_RETURN(char *mutable_suffix,
+                         storage_->GetMutableSuffix(value_index, value_size()));
+  memcpy(mutable_suffix, value, value_size());
+
+  return libtextclassifier3::Status::OK;
 }
 
 bool IcingDynamicTrie::Find(std::string_view key, void *value,
@@ -1832,9 +1862,10 @@
 
   uint32_t best_node_index;
   int key_offset;
-  FindBestNode(key, &best_node_index, &key_offset, false);
+  libtextclassifier3::Status status =
+      FindBestNode(key, &best_node_index, &key_offset, false);
 
-  if (key_offset < 0) {
+  if (key_offset < 0 || !status.ok()) {
     return false;
   }
 
@@ -1929,7 +1960,8 @@
   // Find node matching prefix.
   uint32_t node_index;
   int key_offset;
-  trie_.FindBestNode(cur_key_, &node_index, &key_offset, true);
+  libtextclassifier3::Status status =
+      trie_.FindBestNode(cur_key_, &node_index, &key_offset, true);
 
   // Two cases/states:
   //
@@ -1940,9 +1972,8 @@
   //   prefix. Check that suffix matches the prefix. Then we set
   //   single_leaf_match_ = true and apply different logic for
   //   Advance.
-  if (key_offset < 0) {
-    // A negative key_offset indicates that trie_.storage_ is empty
-    ICING_LOG(FATAL) << "Trie storage is empty";
+  if (key_offset < 0 || !status.ok()) {
+    ICING_LOG(FATAL) << "FindBestNode failed: " << status.error_message();
   }
 
   const Node *best_node = trie_.storage_->GetNode(node_index);
@@ -2179,8 +2210,8 @@
 
 bool IcingDynamicTrie::Utf8Iterator::IsValid() const { return cur_len_ > 0; }
 
-const IcingDynamicTrie::Next *IcingDynamicTrie::GetNextByChar(
-    const Node *node, uint8_t key_char) const {
+libtextclassifier3::StatusOr<const IcingDynamicTrie::Next*>
+IcingDynamicTrie::GetNextByChar(const Node* node, uint8_t key_char) const {
   const Next *next_start = storage_->GetNext(node->next_index(), 0);
   const Next *next_end = next_start + (1 << node->log2_num_children());
 
@@ -2190,6 +2221,11 @@
     return nullptr;
   }
 
+  if (found->node_index() >= storage_->GetNodeArraySize()) {
+    return absl_ports::InternalError(
+        "Node index is out of bounds. The index may be corrupted.");
+  }
+
   return found;
 }
 
@@ -2229,9 +2265,9 @@
   }
 }
 
-void IcingDynamicTrie::FindBestNode(std::string_view key,
-                                    uint32_t *best_node_index, int *key_offset,
-                                    bool prefix, bool utf8) const {
+libtextclassifier3::Status IcingDynamicTrie::FindBestNode(
+    std::string_view key, uint32_t* best_node_index, int* key_offset,
+    bool prefix, bool utf8) const {
   // Find the best node such that:
   //
   // - If key is NOT in the trie, key[0..key_offset) is a prefix to
@@ -2247,7 +2283,7 @@
   if (storage_->empty()) {
     *best_node_index = 0;
     *key_offset = -1;
-    return;
+    return absl_ports::InternalError("Trie is empty.");
   }
 
   const Node *cur_node = storage_->GetRootNode();
@@ -2256,7 +2292,8 @@
   const Node *utf8_node = cur_node;
   while (!cur_node->is_leaf()) {
     char cur_char = GetCharOrNull(key, cur_key_idx);
-    const Next *found = GetNextByChar(cur_node, cur_char);
+    ICING_ASSIGN_OR_RETURN(const Next* found,
+                           GetNextByChar(cur_node, cur_char));
     if (!found) break;
 
     if (prefix && found->val() == 0) {
@@ -2287,10 +2324,13 @@
 
   *best_node_index = storage_->GetNodeIndex(cur_node);
   *key_offset = cur_key_idx;
+
+  return libtextclassifier3::Status::OK;
 }
 
-int IcingDynamicTrie::FindNewBranchingPrefixLength(std::string_view key,
-                                                   bool utf8) const {
+libtextclassifier3::StatusOr<int>
+IcingDynamicTrie::FindNewBranchingPrefixLength(std::string_view key,
+                                               bool utf8) const {
   if (!IsKeyValid(key)) {
     return kNoBranchFound;
   }
@@ -2301,7 +2341,8 @@
 
   uint32_t best_node_index;
   int key_offset;
-  FindBestNode(key, &best_node_index, &key_offset, /*prefix=*/true, utf8);
+  ICING_RETURN_IF_ERROR(
+      FindBestNode(key, &best_node_index, &key_offset, /*prefix=*/true, utf8));
   if (key_offset < 0) {
     return kNoBranchFound;
   }
@@ -2353,8 +2394,9 @@
   return kNoBranchFound;
 }
 
-std::vector<int> IcingDynamicTrie::FindBranchingPrefixLengths(
-    std::string_view key, bool utf8) const {
+libtextclassifier3::StatusOr<std::vector<int>>
+IcingDynamicTrie::FindBranchingPrefixLengths(std::string_view key,
+                                             bool utf8) const {
   std::vector<int> prefix_lengths;
 
   if (!IsKeyValid(key)) {
@@ -2383,7 +2425,8 @@
     }
 
     // Move to next.
-    const Next *found = GetNextByChar(cur_node, key[idx]);
+    ICING_ASSIGN_OR_RETURN(const Next* found,
+                           GetNextByChar(cur_node, key[idx]));
     if (found == nullptr) {
       break;
     }
@@ -2394,7 +2437,8 @@
   return prefix_lengths;
 }
 
-bool IcingDynamicTrie::IsBranchingTerm(std::string_view key) const {
+libtextclassifier3::StatusOr<bool> IcingDynamicTrie::IsBranchingTerm(
+    std::string_view key) const {
   if (!is_initialized()) {
     ICING_LOG(FATAL) << "DynamicTrie not initialized";
   }
@@ -2409,7 +2453,8 @@
 
   uint32_t best_node_index;
   int key_offset;
-  FindBestNode(key, &best_node_index, &key_offset, /*prefix=*/true);
+  ICING_RETURN_IF_ERROR(
+      FindBestNode(key, &best_node_index, &key_offset, /*prefix=*/true));
   if (key_offset < 0) {
     return false;
   }
@@ -2428,7 +2473,8 @@
   // Found key as an intermediate node, but key is not a valid term stored in
   // the trie. In this case, we need at least two children for key to be a
   // branching term.
-  if (GetNextByChar(cur_node, '\0') == nullptr) {
+  ICING_ASSIGN_OR_RETURN(const Next* next, GetNextByChar(cur_node, '\0'));
+  if (next == nullptr) {
     return cur_node->log2_num_children() >= 1;
   }
 
@@ -2620,19 +2666,18 @@
 //        element will cause a crash or fetch incorrect data.
 //    - So we must reset the trie state to make sure storage_->empty() works
 //      correctly and prevents trie APIs from accessing the root node.
-bool IcingDynamicTrie::Delete(std::string_view key) {
+libtextclassifier3::Status IcingDynamicTrie::Delete(std::string_view key) {
   if (!is_initialized()) {
-    ICING_LOG(ERROR) << "DynamicTrie not initialized";
-    return false;
+    return absl_ports::FailedPreconditionError("DynamicTrie not initialized");
   }
 
   if (!IsKeyValid(key)) {
-    return false;
+    return absl_ports::InvalidArgumentError("Invalid key");
   }
 
   if (storage_->empty()) {
     // Nothing to delete.
-    return true;
+    return libtextclassifier3::Status::OK;
   }
 
   // Tries to find the key in the trie, starting from the root.
@@ -2656,7 +2701,7 @@
       // Leaf node, now check the suffix.
       if (key.substr(i) != storage_->GetSuffix(current_node->next_index())) {
         // Key does not exist in the trie, nothing to delete.
-        return true;
+        return libtextclassifier3::Status::OK;
       }
       // Otherwise, key is found.
       break;
@@ -2667,27 +2712,29 @@
     if (i == key.length()) {
       // When we're at the end of the key, the next char is the termination char
       // '\0'.
-      next = GetNextByChar(current_node, '\0');
+      ICING_ASSIGN_OR_RETURN(next, GetNextByChar(current_node, '\0'));
     } else {
-      next = GetNextByChar(current_node, key[i]);
+      ICING_ASSIGN_OR_RETURN(next, GetNextByChar(current_node, key[i]));
     }
 
     if (next == nullptr) {
       // Key does not exist in the trie, nothing to delete.
-      return true;
+      return libtextclassifier3::Status::OK;
     }
 
     // Checks the real size of next array.
     uint32_t next_array_buffer_size = 1u << current_node->log2_num_children();
-    Next *next_array_start = storage_->GetMutableNextArray(
-        current_node->next_index(), next_array_buffer_size);
+    ICING_ASSIGN_OR_RETURN(
+        Next * next_array_start,
+        storage_->GetMutableNextArray(current_node->next_index(),
+                                      next_array_buffer_size));
     int valid_next_array_size =
         GetValidNextsSize(next_array_start, next_array_buffer_size);
     if (valid_next_array_size == 0) {
       // Key does not exist in the trie, nothing to delete.
       // This shouldn't happen, but we put a sanity check here in case something
       // is wrong.
-      return true;
+      return libtextclassifier3::Status::OK;
     } else if (valid_next_array_size == 1) {
       // Single-child branch will be deleted.
       nexts_to_reset.push_back(storage_->GetNextArrayIndex(next));
@@ -2704,25 +2751,28 @@
   }
   // Now we've found the key in the trie.
 
-  ClearSuffixAndValue(current_node->next_index());
+  ICING_RETURN_IF_ERROR(ClearSuffixAndValue(current_node->next_index()));
 
   // Resets nexts to remove key information.
   for (uint32_t next_index : nexts_to_reset) {
-    ResetNext(next_index);
+    ICING_RETURN_IF_ERROR(ResetNext(next_index));
   }
 
   if (last_multichild_node != nullptr) {
-    SortNextArray(last_multichild_node);
+    ICING_RETURN_IF_ERROR(SortNextArray(last_multichild_node));
     uint32_t next_array_buffer_size =
         1u << last_multichild_node->log2_num_children();
-    Next *next_array_start = this->storage_->GetMutableNextArray(
-        last_multichild_node->next_index(), next_array_buffer_size);
+    ICING_ASSIGN_OR_RETURN(
+        Next * next_array_start,
+        this->storage_->GetMutableNextArray(last_multichild_node->next_index(),
+                                            next_array_buffer_size));
     uint32_t num_children =
         GetValidNextsSize(next_array_start, next_array_buffer_size);
     // Shrink the next array if we can.
     if (num_children == next_array_buffer_size / 2) {
-      Node *mutable_node = storage_->GetMutableNode(
-          storage_->GetNodeIndex(last_multichild_node));
+      ICING_ASSIGN_OR_RETURN(Node * mutable_node,
+                             storage_->GetMutableNode(
+                                 storage_->GetNodeIndex(last_multichild_node)));
       mutable_node->set_log2_num_children(mutable_node->log2_num_children() -
                                           1);
       // Add the unused second half of the next array to the free list.
@@ -2744,7 +2794,7 @@
     Clear();
   }
 
-  return true;
+  return libtextclassifier3::Status::OK;
 }
 
 bool IcingDynamicTrie::ClearPropertyForAllValues(uint32_t property_id) {
diff --git a/icing/legacy/index/icing-dynamic-trie.h b/icing/legacy/index/icing-dynamic-trie.h
index 53d9bad..a741264 100644
--- a/icing/legacy/index/icing-dynamic-trie.h
+++ b/icing/legacy/index/icing-dynamic-trie.h
@@ -35,23 +35,21 @@
 #ifndef ICING_LEGACY_INDEX_ICING_DYNAMIC_TRIE_H_
 #define ICING_LEGACY_INDEX_ICING_DYNAMIC_TRIE_H_
 
+#include <cstddef>
 #include <cstdint>
 #include <memory>
+#include <ostream>
 #include <string>
 #include <string_view>
-#include <unordered_map>
 #include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
-#include "icing/legacy/core/icing-compat.h"
 #include "icing/legacy/core/icing-packed-pod.h"
 #include "icing/legacy/index/icing-filesystem.h"
-#include "icing/legacy/index/icing-mmapper.h"
 #include "icing/legacy/index/icing-storage.h"
 #include "icing/legacy/index/proto/icing-dynamic-trie-header.pb.h"
 #include "icing/util/crc32.h"
-#include "icing/util/i18n-utils.h"
 #include "unicode/utf8.h"
 
 namespace icing {
@@ -299,15 +297,15 @@
   // Empty out the trie without closing or removing.
   void Clear();
 
-  // Clears the suffix and value at the given index. Returns true on success.
-  bool ClearSuffixAndValue(uint32_t suffix_value_index);
+  // Clears the suffix and value at the given index. Returns OK on success.
+  libtextclassifier3::Status ClearSuffixAndValue(uint32_t suffix_value_index);
 
   // Resets the next at the given index so that it points to no node.
-  // Returns true on success.
-  bool ResetNext(uint32_t next_index);
+  // Returns OK on success.
+  libtextclassifier3::Status ResetNext(uint32_t next_index);
 
-  // Sorts the next array of the node. Returns true on success.
-  bool SortNextArray(const Node *node);
+  // Sorts the next array of the node. Returns OK on success.
+  libtextclassifier3::Status SortNextArray(const Node *node);
 
   // Sync to disk.
   bool Sync() override;
@@ -348,7 +346,8 @@
   // value.
   //
   // REQUIRES: value a buffer of size value_size()
-  void SetValueAtIndex(uint32_t value_index, const void *value);
+  libtextclassifier3::Status SetValueAtIndex(uint32_t value_index,
+                                             const void *value);
 
   // Returns true if key is found and sets value. If value_index is
   // not NULL, returns value_index (see Insert discussion above).
@@ -384,19 +383,21 @@
   // Return prefix of any new branches created if key were inserted. If utf8 is
   // true, does not cut key mid-utf8. Returns kNoBranchFound if no branches
   // would be created.
-  int FindNewBranchingPrefixLength(std::string_view key, bool utf8) const;
+  libtextclassifier3::StatusOr<int> FindNewBranchingPrefixLength(
+      std::string_view key, bool utf8) const;
 
   // Find all prefixes of key where the trie branches. Excludes the key
   // itself. If utf8 is true, does not cut key mid-utf8.
-  std::vector<int> FindBranchingPrefixLengths(std::string_view key,
-                                              bool utf8) const;
+  libtextclassifier3::StatusOr<std::vector<int>> FindBranchingPrefixLengths(
+      std::string_view key, bool utf8) const;
 
   // Check if key is a branching term.
   //
   // key is a branching term, if and only if there exists terms s1 and s2 in the
   // trie such that key is the maximum common prefix of s1 and s2, but s1 and s2
   // are not prefixes of each other.
-  bool IsBranchingTerm(std::string_view key) const;
+  libtextclassifier3::StatusOr<bool> IsBranchingTerm(
+      std::string_view key) const;
 
   void GetDebugInfo(int verbosity, std::string *out) const override;
 
@@ -431,8 +432,8 @@
   bool ClearDeleted(uint32_t value_index);
 
   // Deletes the entry associated with the key. Data can not be recovered after
-  // the deletion. Returns true on success.
-  bool Delete(std::string_view key);
+  // the deletion. Returns OK on success.
+  libtextclassifier3::Status Delete(std::string_view key);
 
   // Clear a specific property id from all values.  For each value that has this
   // property cleared, also check to see if it was the only property set;  if
@@ -630,14 +631,17 @@
                              uint32_t depth = 0) const;
 
   // Helpers for Find and Insert.
-  const Next *GetNextByChar(const Node *node, uint8_t key_char) const;
+  libtextclassifier3::StatusOr<const Next*> GetNextByChar(
+      const Node* node, uint8_t key_char) const;
   const Next *LowerBound(const Next *start, const Next *end, uint8_t key_char,
                          uint32_t node_index = 0) const;
   // Returns the number of valid nexts in the array.
   int GetValidNextsSize(const IcingDynamicTrie::Next *next_array_start,
                         int next_array_length) const;
-  void FindBestNode(std::string_view key, uint32_t *best_node_index,
-                    int *key_offset, bool prefix, bool utf8 = false) const;
+  libtextclassifier3::Status FindBestNode(std::string_view key,
+                                          uint32_t* best_node_index,
+                                          int* key_offset, bool prefix,
+                                          bool utf8 = false) const;
 
   // For value properties.  This truncates the data by clearing it, but leaving
   // the storage intact.
diff --git a/icing/legacy/index/icing-dynamic-trie_test.cc b/icing/legacy/index/icing-dynamic-trie_test.cc
index 778a1a9..050ff32 100644
--- a/icing/legacy/index/icing-dynamic-trie_test.cc
+++ b/icing/legacy/index/icing-dynamic-trie_test.cc
@@ -14,14 +14,20 @@
 
 #include "icing/legacy/index/icing-dynamic-trie.h"
 
+#include <algorithm>
 #include <cstddef>
 #include <cstdint>
 #include <cstdio>
+#include <cstring>
+#include <random>
+#include <sstream>
 #include <string>
+#include <string_view>
 #include <unordered_set>
 #include <utility>
 #include <vector>
 
+#include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/hash/farmhash.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
@@ -30,6 +36,7 @@
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/random-string.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/crc32.h"
 #include "icing/util/logging.h"
 
 namespace icing {
@@ -952,7 +959,7 @@
   ASSERT_THAT(trie, Not(IsEmpty()));
 
   // Deletes the key.
-  EXPECT_TRUE(trie.Delete("foo"));
+  ICING_EXPECT_OK(trie.Delete("foo"));
   EXPECT_FALSE(trie.Find("foo", &value));
   EXPECT_THAT(trie, SizeIs(0));  // Explicitly test size() method.
   EXPECT_THAT(trie, IsEmpty());
@@ -982,7 +989,7 @@
   ASSERT_THAT(trie, Not(IsEmpty()));
 
   // Deletes "bar". "r" is a leaf node in the trie.
-  EXPECT_TRUE(trie.Delete("bar"));
+  ICING_EXPECT_OK(trie.Delete("bar"));
   EXPECT_FALSE(trie.Find("bar", &value));
   EXPECT_TRUE(trie.Find("ba", &value));
   EXPECT_THAT(trie, SizeIs(1));
@@ -1013,7 +1020,7 @@
   ASSERT_THAT(trie, Not(IsEmpty()));
 
   // Deletes "ba" which is a key with termination node in the trie.
-  EXPECT_TRUE(trie.Delete("ba"));
+  ICING_EXPECT_OK(trie.Delete("ba"));
   EXPECT_FALSE(trie.Find("ba", &value));
   EXPECT_TRUE(trie.Find("bar", &value));
   EXPECT_THAT(trie, SizeIs(1));
@@ -1046,7 +1053,7 @@
   ASSERT_THAT(trie, Not(IsEmpty()));
 
   // Deletes "bc".
-  EXPECT_TRUE(trie.Delete("bc"));
+  ICING_EXPECT_OK(trie.Delete("bc"));
   EXPECT_FALSE(trie.Find("bc", &value));
   EXPECT_TRUE(trie.Find("ba", &value));
   EXPECT_TRUE(trie.Find("bb", &value));
@@ -1087,7 +1094,7 @@
   ASSERT_THAT(trie, Not(IsEmpty()));
 
   // Deletes "batter".
-  EXPECT_TRUE(trie.Delete("batter"));
+  ICING_EXPECT_OK(trie.Delete("batter"));
   EXPECT_FALSE(trie.Find("batter", &value));
   EXPECT_TRUE(trie.Find("battle", &value));
   EXPECT_TRUE(trie.Find("bar", &value));
@@ -1115,9 +1122,9 @@
   ASSERT_THAT(trie, Not(IsEmpty()));
 
   // Delete "foo", "bar", "baz".
-  EXPECT_TRUE(trie.Delete("foo"));
-  EXPECT_TRUE(trie.Delete("bar"));
-  EXPECT_TRUE(trie.Delete("baz"));
+  ICING_EXPECT_OK(trie.Delete("foo"));
+  ICING_EXPECT_OK(trie.Delete("bar"));
+  ICING_EXPECT_OK(trie.Delete("baz"));
 
   EXPECT_THAT(trie, SizeIs(0));  // Explicitly test size() method.
   EXPECT_THAT(trie, IsEmpty());
@@ -1142,7 +1149,7 @@
   ASSERT_THAT(trie.Insert("foo", &value), IsOk());
 
   // Deletes a key
-  ASSERT_TRUE(trie.Delete("bed"));
+  ICING_ASSERT_OK(trie.Delete("bed"));
   ASSERT_FALSE(trie.Find("bed", &value));
 
   // Inserts after deletion
@@ -1166,7 +1173,7 @@
   ASSERT_THAT(trie.Insert("foo", &value), IsOk());
 
   // Deletes a key
-  ASSERT_TRUE(trie.Delete("bed"));
+  ICING_ASSERT_OK(trie.Delete("bed"));
 
   // Iterates through all keys
   IcingDynamicTrie::Iterator iterator_all(trie, "");
@@ -1199,9 +1206,9 @@
   ASSERT_THAT(trie.Insert("foo", &value), IsOk());
 
   // Deletes all keys
-  ASSERT_TRUE(trie.Delete("bar"));
-  ASSERT_TRUE(trie.Delete("bed"));
-  ASSERT_TRUE(trie.Delete("foo"));
+  ICING_ASSERT_OK(trie.Delete("bar"));
+  ICING_ASSERT_OK(trie.Delete("bed"));
+  ICING_ASSERT_OK(trie.Delete("foo"));
 
   EXPECT_THAT(trie, IsEmpty());
 
@@ -1235,8 +1242,8 @@
   ASSERT_THAT(trie.Insert("bed", &value), IsOk());
 
   // "ba" and bedroom are not keys in the trie.
-  EXPECT_TRUE(trie.Delete("ba"));
-  EXPECT_TRUE(trie.Delete("bedroom"));
+  ICING_EXPECT_OK(trie.Delete("ba"));
+  ICING_EXPECT_OK(trie.Delete("bedroom"));
 
   // The original keys are not affected.
   EXPECT_TRUE(trie.Find("bar", &value));
@@ -1258,7 +1265,7 @@
   ASSERT_THAT(trie.Insert("fjord", &value), IsOk());
 
   // Delete the third child
-  EXPECT_TRUE(trie.Delete("foul"));
+  ICING_EXPECT_OK(trie.Delete("foul"));
 
   std::vector<std::string> remaining;
   for (IcingDynamicTrie::Iterator term_iter(trie, /*prefix=*/"");
@@ -1282,7 +1289,7 @@
   ASSERT_THAT(trie.Insert("fudge", &value), IsOk());
 
   // Delete the second child
-  EXPECT_TRUE(trie.Delete("foul"));
+  ICING_EXPECT_OK(trie.Delete("foul"));
 
   std::vector<std::string> remaining;
   for (IcingDynamicTrie::Iterator term_iter(trie, /*prefix=*/"");
@@ -1314,7 +1321,7 @@
   std::shuffle(terms.begin(), terms.end(), random);
   for (int i = 0; i < 1024; ++i) {
     exp_remaining.erase(terms[i]);
-    ASSERT_TRUE(trie.Delete(terms[i]));
+    ICING_ASSERT_OK(trie.Delete(terms[i]));
   }
 
   // Check that the iterator still works, and the remaining terms are correct.
@@ -1523,68 +1530,68 @@
   uint32_t value = 1;
 
   ASSERT_THAT(trie.Insert("", &value), IsOk());
-  EXPECT_FALSE(trie.IsBranchingTerm(""));
+  EXPECT_FALSE(trie.IsBranchingTerm("").ValueOrDie());
 
   ASSERT_THAT(trie.Insert("ab", &value), IsOk());
-  EXPECT_FALSE(trie.IsBranchingTerm(""));
-  EXPECT_FALSE(trie.IsBranchingTerm("ab"));
+  EXPECT_FALSE(trie.IsBranchingTerm("").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ab").ValueOrDie());
 
   ASSERT_THAT(trie.Insert("ac", &value), IsOk());
   // "" is a prefix of "ab" and "ac", but it is not a branching term.
-  EXPECT_FALSE(trie.IsBranchingTerm(""));
-  EXPECT_FALSE(trie.IsBranchingTerm("ab"));
-  EXPECT_FALSE(trie.IsBranchingTerm("ac"));
+  EXPECT_FALSE(trie.IsBranchingTerm("").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ab").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ac").ValueOrDie());
 
   ASSERT_THAT(trie.Insert("ba", &value), IsOk());
   // "" now branches to "ba"
-  EXPECT_TRUE(trie.IsBranchingTerm(""));
-  EXPECT_FALSE(trie.IsBranchingTerm("ab"));
-  EXPECT_FALSE(trie.IsBranchingTerm("ac"));
-  EXPECT_FALSE(trie.IsBranchingTerm("ba"));
+  EXPECT_TRUE(trie.IsBranchingTerm("").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ab").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ac").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ba").ValueOrDie());
 
   ASSERT_THAT(trie.Insert("a", &value), IsOk());
-  EXPECT_TRUE(trie.IsBranchingTerm(""));
+  EXPECT_TRUE(trie.IsBranchingTerm("").ValueOrDie());
   // "a" branches to "ab" and "ac"
-  EXPECT_TRUE(trie.IsBranchingTerm("a"));
-  EXPECT_FALSE(trie.IsBranchingTerm("ab"));
-  EXPECT_FALSE(trie.IsBranchingTerm("ac"));
-  EXPECT_FALSE(trie.IsBranchingTerm("ba"));
+  EXPECT_TRUE(trie.IsBranchingTerm("a").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ab").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ac").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ba").ValueOrDie());
 
   ASSERT_THAT(trie.Insert("abc", &value), IsOk());
   ASSERT_THAT(trie.Insert("acd", &value), IsOk());
-  EXPECT_TRUE(trie.IsBranchingTerm(""));
-  EXPECT_TRUE(trie.IsBranchingTerm("a"));
+  EXPECT_TRUE(trie.IsBranchingTerm("").ValueOrDie());
+  EXPECT_TRUE(trie.IsBranchingTerm("a").ValueOrDie());
   // "ab" is a prefix of "abc", but it is not a branching term.
-  EXPECT_FALSE(trie.IsBranchingTerm("ab"));
+  EXPECT_FALSE(trie.IsBranchingTerm("ab").ValueOrDie());
   // "ac" is a prefix of "acd", but it is not a branching term.
-  EXPECT_FALSE(trie.IsBranchingTerm("ac"));
-  EXPECT_FALSE(trie.IsBranchingTerm("ba"));
-  EXPECT_FALSE(trie.IsBranchingTerm("abc"));
-  EXPECT_FALSE(trie.IsBranchingTerm("acd"));
+  EXPECT_FALSE(trie.IsBranchingTerm("ac").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ba").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("abc").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("acd").ValueOrDie());
 
   ASSERT_THAT(trie.Insert("abcd", &value), IsOk());
-  EXPECT_TRUE(trie.IsBranchingTerm(""));
-  EXPECT_TRUE(trie.IsBranchingTerm("a"));
+  EXPECT_TRUE(trie.IsBranchingTerm("").ValueOrDie());
+  EXPECT_TRUE(trie.IsBranchingTerm("a").ValueOrDie());
   // "ab" is a prefix of "abc" and "abcd", but it is not a branching term.
-  EXPECT_FALSE(trie.IsBranchingTerm("ab"));
-  EXPECT_FALSE(trie.IsBranchingTerm("ac"));
-  EXPECT_FALSE(trie.IsBranchingTerm("ba"));
+  EXPECT_FALSE(trie.IsBranchingTerm("ab").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ac").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ba").ValueOrDie());
   // "abc" is a prefix of "abcd", but it is not a branching term.
-  EXPECT_FALSE(trie.IsBranchingTerm("abc"));
-  EXPECT_FALSE(trie.IsBranchingTerm("acd"));
-  EXPECT_FALSE(trie.IsBranchingTerm("abcd"));
+  EXPECT_FALSE(trie.IsBranchingTerm("abc").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("acd").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("abcd").ValueOrDie());
 
   ASSERT_THAT(trie.Insert("abd", &value), IsOk());
-  EXPECT_TRUE(trie.IsBranchingTerm(""));
-  EXPECT_TRUE(trie.IsBranchingTerm("a"));
+  EXPECT_TRUE(trie.IsBranchingTerm("").ValueOrDie());
+  EXPECT_TRUE(trie.IsBranchingTerm("a").ValueOrDie());
   // "ab" branches to "abc" and "abd"
-  EXPECT_TRUE(trie.IsBranchingTerm("ab"));
-  EXPECT_FALSE(trie.IsBranchingTerm("ac"));
-  EXPECT_FALSE(trie.IsBranchingTerm("ba"));
-  EXPECT_FALSE(trie.IsBranchingTerm("abc"));
-  EXPECT_FALSE(trie.IsBranchingTerm("acd"));
-  EXPECT_FALSE(trie.IsBranchingTerm("abcd"));
-  EXPECT_FALSE(trie.IsBranchingTerm("abd"));
+  EXPECT_TRUE(trie.IsBranchingTerm("ab").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ac").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ba").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("abc").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("acd").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("abcd").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("abd").ValueOrDie());
 }
 
 TEST_F(IcingDynamicTrieTest, IsBranchingTermShouldWorkForNonExistingTerms) {
@@ -1596,58 +1603,58 @@
 
   uint32_t value = 1;
 
-  EXPECT_FALSE(trie.IsBranchingTerm(""));
-  EXPECT_FALSE(trie.IsBranchingTerm("a"));
-  EXPECT_FALSE(trie.IsBranchingTerm("ab"));
-  EXPECT_FALSE(trie.IsBranchingTerm("abc"));
+  EXPECT_FALSE(trie.IsBranchingTerm("").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("a").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ab").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("abc").ValueOrDie());
 
   ASSERT_THAT(trie.Insert("aa", &value), IsOk());
-  EXPECT_FALSE(trie.IsBranchingTerm(""));
-  EXPECT_FALSE(trie.IsBranchingTerm("a"));
-  EXPECT_FALSE(trie.IsBranchingTerm("ab"));
-  EXPECT_FALSE(trie.IsBranchingTerm("abc"));
+  EXPECT_FALSE(trie.IsBranchingTerm("").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("a").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ab").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("abc").ValueOrDie());
 
   ASSERT_THAT(trie.Insert("ac", &value), IsOk());
-  EXPECT_FALSE(trie.IsBranchingTerm(""));
+  EXPECT_FALSE(trie.IsBranchingTerm("").ValueOrDie());
   // "a" does not exist in the trie, but now it branches to "aa" and "ac".
-  EXPECT_TRUE(trie.IsBranchingTerm("a"));
-  EXPECT_FALSE(trie.IsBranchingTerm("ab"));
-  EXPECT_FALSE(trie.IsBranchingTerm("abc"));
+  EXPECT_TRUE(trie.IsBranchingTerm("a").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ab").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("abc").ValueOrDie());
 
   ASSERT_THAT(trie.Insert("ad", &value), IsOk());
-  EXPECT_FALSE(trie.IsBranchingTerm(""));
-  EXPECT_TRUE(trie.IsBranchingTerm("a"));
-  EXPECT_FALSE(trie.IsBranchingTerm("ab"));
-  EXPECT_FALSE(trie.IsBranchingTerm("abc"));
+  EXPECT_FALSE(trie.IsBranchingTerm("").ValueOrDie());
+  EXPECT_TRUE(trie.IsBranchingTerm("a").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ab").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("abc").ValueOrDie());
 
   ASSERT_THAT(trie.Insert("abcd", &value), IsOk());
-  EXPECT_FALSE(trie.IsBranchingTerm(""));
-  EXPECT_TRUE(trie.IsBranchingTerm("a"));
-  EXPECT_FALSE(trie.IsBranchingTerm("ab"));
-  EXPECT_FALSE(trie.IsBranchingTerm("abc"));
+  EXPECT_FALSE(trie.IsBranchingTerm("").ValueOrDie());
+  EXPECT_TRUE(trie.IsBranchingTerm("a").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("ab").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("abc").ValueOrDie());
 
   ASSERT_THAT(trie.Insert("abd", &value), IsOk());
-  EXPECT_FALSE(trie.IsBranchingTerm(""));
-  EXPECT_TRUE(trie.IsBranchingTerm("a"));
+  EXPECT_FALSE(trie.IsBranchingTerm("").ValueOrDie());
+  EXPECT_TRUE(trie.IsBranchingTerm("a").ValueOrDie());
   // "ab" does not exist in the trie, but now it branches to "abcd" and "abd".
-  EXPECT_TRUE(trie.IsBranchingTerm("ab"));
-  EXPECT_FALSE(trie.IsBranchingTerm("abc"));
+  EXPECT_TRUE(trie.IsBranchingTerm("ab").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("abc").ValueOrDie());
 
   ASSERT_THAT(trie.Insert("abce", &value), IsOk());
-  EXPECT_FALSE(trie.IsBranchingTerm(""));
-  EXPECT_TRUE(trie.IsBranchingTerm("a"));
-  EXPECT_TRUE(trie.IsBranchingTerm("ab"));
+  EXPECT_FALSE(trie.IsBranchingTerm("").ValueOrDie());
+  EXPECT_TRUE(trie.IsBranchingTerm("a").ValueOrDie());
+  EXPECT_TRUE(trie.IsBranchingTerm("ab").ValueOrDie());
   // "abc" does not exist in the trie, but now it branches to "abcd" and "abce".
-  EXPECT_TRUE(trie.IsBranchingTerm("abc"));
+  EXPECT_TRUE(trie.IsBranchingTerm("abc").ValueOrDie());
 
   ASSERT_THAT(trie.Insert("abc_suffix", &value), IsOk());
-  EXPECT_FALSE(trie.IsBranchingTerm(""));
-  EXPECT_TRUE(trie.IsBranchingTerm("a"));
-  EXPECT_TRUE(trie.IsBranchingTerm("ab"));
-  EXPECT_TRUE(trie.IsBranchingTerm("abc"));
-  EXPECT_FALSE(trie.IsBranchingTerm("abc_s"));
-  EXPECT_FALSE(trie.IsBranchingTerm("abc_su"));
-  EXPECT_FALSE(trie.IsBranchingTerm("abc_suffi"));
+  EXPECT_FALSE(trie.IsBranchingTerm("").ValueOrDie());
+  EXPECT_TRUE(trie.IsBranchingTerm("a").ValueOrDie());
+  EXPECT_TRUE(trie.IsBranchingTerm("ab").ValueOrDie());
+  EXPECT_TRUE(trie.IsBranchingTerm("abc").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("abc_s").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("abc_su").ValueOrDie());
+  EXPECT_FALSE(trie.IsBranchingTerm("abc_suffi").ValueOrDie());
 }
 
 }  // namespace lib
diff --git a/icing/legacy/index/icing-filesystem.cc b/icing/legacy/index/icing-filesystem.cc
index 175e075..168361d 100644
--- a/icing/legacy/index/icing-filesystem.cc
+++ b/icing/legacy/index/icing-filesystem.cc
@@ -129,15 +129,21 @@
                            const std::unordered_set<std::string> &exclude,
                            bool recursive, const char *prefix,
                            std::vector<std::string> *entries) {
-  DIR *dir = opendir(dir_name);
-  if (!dir) {
+  auto closer = [](DIR *dir) {
+    if (closedir(dir) != 0) {
+      ICING_LOG(ERROR) << "Error closing dir (" << errno << ") "
+                       << strerror(errno);
+    }
+  };
+  std::unique_ptr<DIR, decltype(closer)> dir(opendir(dir_name), closer);
+  if (dir == nullptr) {
     LogOpenError("Unable to open directory ", dir_name, ": ", errno);
     return false;
   }
 
   dirent *p;
   // readdir's implementation seems to be thread safe.
-  while ((p = readdir(dir)) != nullptr) {
+  while ((p = readdir(dir.get())) != nullptr) {
     std::string file_name(p->d_name);
     if (file_name == "." || file_name == ".." ||
         exclude.find(file_name) != exclude.end()) {
@@ -156,9 +162,6 @@
       }
     }
   }
-  if (closedir(dir) != 0) {
-    ICING_LOG(ERROR) << "Error closing " << dir_name << ": " << strerror(errno);
-  }
   return true;
 }
 
@@ -445,9 +448,12 @@
 bool IcingFilesystem::Write(int fd, const void *data, size_t data_size) const {
   size_t write_len = data_size;
   do {
-    // Don't try to write too much at once.
-    size_t chunk_size = std::min<size_t>(write_len, 64u * 1024);
-    ssize_t wrote = write(fd, data, chunk_size);
+#ifdef __APPLE__
+    // TEMP_FAILURE_RETRY is not defined in unistd.h on iOS.
+    ssize_t wrote = write(fd, data, write_len);
+#else  // __APPLE__
+    ssize_t wrote = TEMP_FAILURE_RETRY(write(fd, data, write_len));
+#endif // __APPLE__
     if (wrote < 0) {
       ICING_LOG(ERROR) << "Bad write: " << strerror(errno);
       return false;
@@ -462,9 +468,12 @@
                              size_t data_size) const {
   size_t write_len = data_size;
   do {
-    // Don't try to write too much at once.
-    size_t chunk_size = std::min<size_t>(write_len, 64u * 1024);
-    ssize_t wrote = pwrite(fd, data, chunk_size, offset);
+#ifdef __APPLE__
+    // TEMP_FAILURE_RETRY is not defined in unistd.h on iOS.
+    ssize_t wrote = pwrite(fd, data, write_len, offset);
+#else  // __APPLE__
+    ssize_t wrote = TEMP_FAILURE_RETRY(pwrite(fd, data, write_len, offset));
+#endif // __APPLE__
     if (wrote < 0) {
       ICING_LOG(ERROR) << "Bad write: " << strerror(errno);
       return false;
diff --git a/icing/monkey_test/icing-monkey-test-runner.cc b/icing/monkey_test/icing-monkey-test-runner.cc
index 14b4e02..c16a5d9 100644
--- a/icing/monkey_test/icing-monkey-test-runner.cc
+++ b/icing/monkey_test/icing-monkey-test-runner.cc
@@ -20,6 +20,7 @@
 #include <functional>
 #include <iomanip>
 #include <ios>
+#include <limits>
 #include <memory>
 #include <random>
 #include <sstream>
@@ -30,7 +31,6 @@
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "icing/absl_ports/str_cat.h"
-#include "icing/file/destructible-directory.h"
 #include "icing/icing-search-engine.h"
 #include "icing/monkey_test/in-memory-icing-search-engine.h"
 #include "icing/monkey_test/monkey-test-generators.h"
@@ -67,6 +67,11 @@
   return dist(*random) == 1;
 }
 
+int GetRandomInt(MonkeyTestRandomEngine* random, int min, int max) {
+  std::uniform_int_distribution<> dist(min, max);
+  return dist(*random);
+}
+
 SearchSpecProto GenerateRandomSearchSpecProto(
     MonkeyTestRandomEngine* random,
     MonkeyDocumentGenerator* document_generator) {
@@ -118,6 +123,15 @@
           query);
     }
   }
+  // %50 chance of getting one type filter
+  // %25 chance of getting two type filters
+  // %25 chance of getting no type filters
+  for (int i = 0; i < 2; ++i) {
+    if (GetRandomBoolean(random)) {
+      search_spec.add_schema_type_filters(
+          document_generator->GetType().schema_type());
+    }
+  }
   search_spec.set_query(query);
   return search_spec;
 }
@@ -215,9 +229,7 @@
       schema_generator_(
           std::make_unique<MonkeySchemaGenerator>(&random_, &config_)) {
   ICING_LOG(INFO) << "Monkey test runner started with seed: " << config_.seed;
-  std::string dir = GetTestTempDir() + "/icing/monkey";
-  filesystem_.DeleteDirectoryRecursively(dir.c_str());
-  icing_dir_ = std::make_unique<DestructibleDirectory>(&filesystem_, dir);
+  icing_dir_ = GetTestTempDir() + "/icing/monkey";
 }
 
 void IcingMonkeyTestRunner::Run(uint32_t num) {
@@ -230,7 +242,7 @@
     frequency_sum += schedule.second;
   }
   std::uniform_int_distribution<> dist(0, frequency_sum - 1);
-  for (; num; --num) {
+  for (uint32_t i = 0; i < num; ++i) {
     int p = dist(random_);
     for (const auto& schedule : config_.monkey_api_schedules) {
       if (p < schedule.second) {
@@ -239,7 +251,7 @@
       }
       p -= schedule.second;
     }
-    ICING_LOG(INFO) << "Completed Run #" << num
+    ICING_LOG(INFO) << "Completed Run #" << i
                     << ". Documents in the in-memory icing: "
                     << in_memory_icing_->GetNumAliveDocuments();
   }
@@ -254,12 +266,20 @@
 }
 
 void IcingMonkeyTestRunner::Initialize() {
-  ASSERT_NO_FATAL_FAILURE(CreateIcingSearchEngine());
+  if (config_.initialize_by_existing_data) {
+    ICING_LOG(INFO) << "Initializing icing by existing data";
 
-  SchemaProto schema = schema_generator_->GenerateSchema();
-  ICING_LOG(DBG) << "Schema Generated: " << schema.DebugString();
+    ASSERT_NO_FATAL_FAILURE(CreateIcingSearchEngine());
+    ASSERT_NO_FATAL_FAILURE(ReloadInMemoryIcing());
+  } else {
+    ICING_LOG(INFO) << "Initializing icing by empty data";
 
-  ASSERT_THAT(SetSchema(std::move(schema)).status(), ProtoIsOk());
+    filesystem_.DeleteDirectoryRecursively(icing_dir_.c_str());
+    ASSERT_NO_FATAL_FAILURE(CreateIcingSearchEngine());
+    SchemaProto schema = schema_generator_->GenerateSchema();
+    ICING_LOG(DBG) << "Schema Generated: " << schema.DebugString();
+    ASSERT_THAT(SetSchema(std::move(schema)).status(), ProtoIsOk());
+  }
 }
 
 void IcingMonkeyTestRunner::DoUpdateSchema() {
@@ -544,7 +564,7 @@
 
   IcingSearchEngineOptions icing_options;
   icing_options.set_index_merge_size(config_.index_merge_size);
-  icing_options.set_base_dir(icing_dir_->dir());
+  icing_options.set_base_dir(icing_dir_);
   icing_options.set_optimize_rebuild_index_threshold(
       optimize_rebuild_index_threshold);
   // The method will be called every time when we ReloadFromDisk(), so randomly
@@ -552,10 +572,68 @@
   icing_options.set_document_store_namespace_id_fingerprint(
       GetRandomBoolean(&random_));
   icing_options.set_enable_embedding_index(true);
-  icing_options.set_enable_embedding_quantization(GetRandomBoolean(&random_));
+  icing_options.set_enable_embedding_quantization(true);
+  icing_options.set_compression_threshold_bytes(
+      GetRandomInt(&random_, /*min=*/0, /*max=*/10000));
+  icing_options.set_enable_eigen_embedding_scoring(GetRandomBoolean(&random_));
+  icing_options.set_enable_passing_filter_to_children(
+      GetRandomBoolean(&random_));
+  icing_options.set_enable_embedding_iterator_v2(GetRandomBoolean(&random_));
+  // Randomly choose the number of shards from 1, 2, 4, 8, 16, 32.
+  uint32_t num_shards = 1 << GetRandomInt(&random_, /*min=*/0, /*max=*/5);
+  icing_options.set_embedding_index_num_shards(num_shards);
+  icing_options.set_enable_schema_database(GetRandomBoolean(&random_));
+  icing_options.set_enable_schema_type_id_optimization(
+      GetRandomBoolean(&random_));
+  icing_options.set_enable_skip_set_schema_type_equality_check(
+      GetRandomBoolean(&random_));
+  icing_options.set_enable_embed_query_optimization(GetRandomBoolean(&random_));
   icing_ = std::make_unique<IcingSearchEngine>(icing_options);
   ASSERT_THAT(icing_->Initialize().status(), ProtoIsOk());
 }
 
+void IcingMonkeyTestRunner::ReloadInMemoryIcing() {
+  // Reload schema
+  GetSchemaResultProto get_schema_result = icing_->GetSchema();
+  ASSERT_THAT(get_schema_result.status(), ProtoIsOk());
+  in_memory_icing_->SetSchema(get_schema_result.schema());
+  ASSERT_THAT(*in_memory_icing_->GetSchema(),
+              EqualsProto(get_schema_result.schema()));
+  document_generator_ = std::make_unique<MonkeyDocumentGenerator>(
+      &random_, in_memory_icing_->GetSchema(), &config_);
+
+  // Reload documents
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::PREFIX);
+  search_spec.set_query("");
+  ResultSpecProto result_spec;
+  result_spec.set_num_to_score(std::numeric_limits<int32_t>::max());
+  result_spec.set_num_per_page(100);
+  int num_results = 0;
+  int max_uri = 0;
+  SearchResultProto search_result = icing_->Search(
+      search_spec, ScoringSpecProto::default_instance(), result_spec);
+  ASSERT_THAT(search_result.status(), ProtoIsOk());
+  while (true) {
+    num_results += search_result.results_size();
+    for (const SearchResultProto::ResultProto& doc : search_result.results()) {
+      in_memory_icing_->Put(MonkeyTokenizedDocument::Reload(doc.document()));
+      max_uri = std::max(
+          max_uri, std::stoi(doc.document().uri().substr(
+                       MonkeyDocumentGenerator::kDocumentUriPrefix.size())));
+    }
+    if (search_result.next_page_token() == kInvalidNextPageToken) {
+      break;
+    }
+    search_result = icing_->GetNextPage(search_result.next_page_token());
+    ASSERT_THAT(search_result.status(), ProtoIsOk());
+  }
+  ICING_LOG(INFO) << "Reloaded " << num_results << " documents";
+
+  // Reload generators
+  schema_generator_->ReloadPreviousStatus(*in_memory_icing_->GetSchema());
+  document_generator_->ReloadPreviousStatus(max_uri);
+}
+
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/monkey_test/icing-monkey-test-runner.h b/icing/monkey_test/icing-monkey-test-runner.h
index 10be60c..dcb81f4 100644
--- a/icing/monkey_test/icing-monkey-test-runner.h
+++ b/icing/monkey_test/icing-monkey-test-runner.h
@@ -17,8 +17,8 @@
 
 #include <cstdint>
 #include <memory>
+#include <string>
 
-#include "icing/file/destructible-directory.h"
 #include "icing/file/filesystem.h"
 #include "icing/icing-search-engine.h"
 #include "icing/monkey_test/in-memory-icing-search-engine.h"
@@ -63,7 +63,7 @@
   IcingMonkeyTestRunnerConfiguration config_;
   MonkeyTestRandomEngine random_;
   Filesystem filesystem_;
-  std::unique_ptr<DestructibleDirectory> icing_dir_;
+  std::string icing_dir_;
   std::unique_ptr<InMemoryIcingSearchEngine> in_memory_icing_;
   std::unique_ptr<IcingSearchEngine> icing_;
 
@@ -71,6 +71,8 @@
   std::unique_ptr<MonkeyDocumentGenerator> document_generator_;
 
   void CreateIcingSearchEngine();
+  // Reloads the in-memory icing from the disk.
+  void ReloadInMemoryIcing();
 };
 
 }  // namespace lib
diff --git a/icing/monkey_test/icing-search-engine_monkey_crash_simulation_test.cc b/icing/monkey_test/icing-search-engine_monkey_crash_simulation_test.cc
new file mode 100644
index 0000000..f5547cf
--- /dev/null
+++ b/icing/monkey_test/icing-search-engine_monkey_crash_simulation_test.cc
@@ -0,0 +1,145 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <signal.h>
+
+#include <cstdint>
+#include <limits>
+#include <memory>
+#include <random>
+
+#include "gtest/gtest.h"
+#include "icing/monkey_test/icing-monkey-test-runner.h"
+#include "icing/monkey_test/monkey-test-util.h"
+#include "icing/portable/platform.h"
+#include "icing/proto/debug.pb.h"
+#include "icing/schema/section.h"
+#include "icing/util/logging.h"
+
+namespace icing {
+namespace lib {
+
+constexpr uint32_t kNumCrashesToSimulate = 5;
+constexpr uint32_t kMinRunningTimePerCrashSeconds = 5;
+constexpr uint32_t kMaxRunningTimePerCrashSeconds = 15;
+
+void RunMonkeyTest(uint32_t seed, bool is_first_run, uint32_t num_iterations) {
+  IcingMonkeyTestRunnerConfiguration config(
+      seed,
+      /*num_types=*/30,
+      /*num_namespaces=*/100,
+      /*num_uris=*/1000,
+      /*index_merge_size=*/1024 * 1024,
+      /*initialize_by_existing_data=*/!is_first_run);
+  config.possible_num_properties = {0,
+                                    1,
+                                    2,
+                                    4,
+                                    8,
+                                    16,
+                                    kTotalNumSections / 2,
+                                    kTotalNumSections,
+                                    kTotalNumSections + 1,
+                                    kTotalNumSections * 2};
+  config.possible_num_tokens = {0, 1, 4, 16, 64, 256};
+  config.possible_num_vectors = {0, 1, 4};
+  config.possible_vector_dimensions = {128, 512, 768};
+  config.monkey_api_schedules = {
+      {&IcingMonkeyTestRunner::DoPut, 500},
+      {&IcingMonkeyTestRunner::DoSearch, 200},
+      {&IcingMonkeyTestRunner::DoGet, 70},
+      {&IcingMonkeyTestRunner::DoGetAllNamespaces, 50},
+      {&IcingMonkeyTestRunner::DoDelete, 50},
+      {&IcingMonkeyTestRunner::DoDeleteByNamespace, 50},
+      {&IcingMonkeyTestRunner::DoDeleteBySchemaType, 45},
+      {&IcingMonkeyTestRunner::DoDeleteByQuery, 20},
+      {&IcingMonkeyTestRunner::DoOptimize, 5},
+      {&IcingMonkeyTestRunner::DoUpdateSchema, 5},
+      {&IcingMonkeyTestRunner::ReloadFromDisk, 5}};
+
+  std::unique_ptr<IcingMonkeyTestRunner> runner =
+      std::make_unique<IcingMonkeyTestRunner>(config);
+  ASSERT_NO_FATAL_FAILURE(runner->Initialize());
+  ASSERT_NO_FATAL_FAILURE(runner->Run(num_iterations));
+  runner.reset();
+}
+
+TEST(IcingSearchEngineMonkeyCrashSimulationTest, MonkeyTest) {
+  uint32_t seed = std::random_device()();
+  MonkeyTestRandomEngine random(seed);
+  ICING_LOG(INFO) << "Monkey test crash simulation started with seed: " << seed;
+
+  bool is_first_run = true;
+  for (int i = 0; i < kNumCrashesToSimulate; ++i) {
+    uint32_t child_seed = random();
+    pid_t pid = fork();
+    ASSERT_TRUE(pid >= 0) << "fork() failed!";
+
+    // Child process
+    if (pid == 0) {
+      // Run indefinitely until the parent process kills the child process.
+      ICING_LOG(INFO) << "Child process " << i << " started running";
+      ASSERT_NO_FATAL_FAILURE(
+          RunMonkeyTest(child_seed, is_first_run, /*num_iterations=*/
+                        std::numeric_limits<uint32_t>::max()));
+      _exit(0);
+    }
+
+    // Randomly generate the running time for each crash.
+    std::uniform_int_distribution<uint32_t>
+        running_time_per_crash_seconds_distribution(
+            kMinRunningTimePerCrashSeconds, kMaxRunningTimePerCrashSeconds);
+    sleep(running_time_per_crash_seconds_distribution(random));
+
+    // Check if the child process has exited.
+    int status = 0;
+    pid_t ret = waitpid(pid, &status, WNOHANG);
+    if (ret == pid) {
+      // The child process has already exited for some reason, which is not
+      // expected. We should check the reason and fail the overall test.
+      if (WIFEXITED(status)) {
+        int code = WEXITSTATUS(status);
+        FAIL() << "Monkey test in the child process exited with code " << code;
+      } else if (WIFSIGNALED(status)) {
+        FAIL()
+            << "Monkey test in the child process was signaled early with sig="
+            << WTERMSIG(status);
+      }
+
+      FAIL() << "Monkey test in the child process ended in unknown reason";
+    } else if (ret != 0) {
+      FAIL() << "waitpid() error!";
+    }
+
+    // The child process is still running, which is expected. We can now kill
+    // the child process to simulate a crash.
+    if (kill(pid, SIGKILL) != 0) {
+      FAIL() << "Failed to kill child process " << i;
+    }
+    waitpid(pid, &status, 0);
+    ICING_LOG(INFO) << "Child process killed.";
+
+    is_first_run = false;
+  }
+
+  // Run the monkey test after the crash. Icing search engine should be able to
+  // recover from the crash and continue running normally.
+  ICING_LOG(INFO) << "Running final monkey test after crash simulation.";
+  uint32_t num_iterations = IsAndroidArm() ? 200 : 1000;
+  ASSERT_NO_FATAL_FAILURE(
+      RunMonkeyTest(/*seed=*/random(), is_first_run, num_iterations));
+}
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/monkey_test/icing-search-engine_monkey_test.cc b/icing/monkey_test/icing-search-engine_monkey_test.cc
index 6d87ba9..7764dd2 100644
--- a/icing/monkey_test/icing-search-engine_monkey_test.cc
+++ b/icing/monkey_test/icing-search-engine_monkey_test.cc
@@ -33,7 +33,8 @@
       /*num_types=*/30,
       /*num_namespaces=*/100,
       /*num_uris=*/1000,
-      /*index_merge_size=*/1024 * 1024);
+      /*index_merge_size=*/1024 * 1024,
+      /*initialize_by_existing_data=*/false);
   config.possible_num_properties = {0,
                                     1,
                                     2,
@@ -45,8 +46,8 @@
                                     kTotalNumSections + 1,
                                     kTotalNumSections * 2};
   config.possible_num_tokens = {0, 1, 4, 16, 64, 256};
-  config.possible_num_vectors = {0, 1, 4};
-  config.possible_vector_dimensions = {128, 512, 768};
+  config.possible_num_vectors = {0, 1, 4, 8};
+  config.possible_vector_dimensions = {8, 16, 128, 512, 768};
   config.monkey_api_schedules = {
       {&IcingMonkeyTestRunner::DoPut, 500},
       {&IcingMonkeyTestRunner::DoSearch, 200},
@@ -71,7 +72,8 @@
       /*num_types=*/30,
       /*num_namespaces=*/200,
       /*num_uris=*/100000,
-      /*index_merge_size=*/1024 * 1024);
+      /*index_merge_size=*/1024 * 1024,
+      /*initialize_by_existing_data=*/false);
 
   // Due to the large amount of documents, we need to make each document smaller
   // to finish the test.
diff --git a/icing/monkey_test/in-memory-icing-search-engine.cc b/icing/monkey_test/in-memory-icing-search-engine.cc
index 417153c..262ba32 100644
--- a/icing/monkey_test/in-memory-icing-search-engine.cc
+++ b/icing/monkey_test/in-memory-icing-search-engine.cc
@@ -30,6 +30,7 @@
 #include "icing/absl_ports/str_cat.h"
 #include "icing/absl_ports/str_join.h"
 #include "icing/index/embed/embedding-scorer.h"
+#include "icing/index/embed/quantizer.h"
 #include "icing/monkey_test/monkey-tokenized-document.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
@@ -77,16 +78,37 @@
   return std::make_pair(values[0], values[1]);
 }
 
-bool DoesVectorsMatch(const EmbeddingScorer *scorer,
-                      std::pair<double, double> embedding_search_range,
-                      const PropertyProto::VectorProto &vector1,
-                      const PropertyProto::VectorProto &vector2) {
-  if (vector1.model_signature() != vector2.model_signature() ||
-      vector1.values_size() != vector2.values_size()) {
+bool DoesVectorsMatch(
+    const EmbeddingScorer* scorer,
+    std::pair<double, double> embedding_search_range,
+    EmbeddingIndexingConfig::QuantizationType::Code quantization_type,
+    const PropertyProto::VectorProto& query,
+    const PropertyProto::VectorProto& candidate) {
+  if (query.model_signature() != candidate.model_signature() ||
+      query.values_size() != candidate.values_size()) {
     return false;
   }
-  float score = scorer->Score(vector1.values_size(), vector1.values().data(),
-                              vector2.values().data());
+
+  const int dimension = query.values_size();
+  float score;
+  if (quantization_type == EmbeddingIndexingConfig::QuantizationType::NONE) {
+    score = scorer->Score(dimension, query.values().data(),
+                          candidate.values().data());
+  } else {
+    // Quantize the candidate vector.
+    auto minmax_pair = std::minmax_element(candidate.values().begin(),
+                                           candidate.values().end());
+    Quantizer quantizer =
+        Quantizer::Create(*minmax_pair.first, *minmax_pair.second).ValueOrDie();
+    std::vector<uint8_t> quantized_candidate;
+    quantized_candidate.reserve(dimension);
+    for (float value : candidate.values()) {
+      quantized_candidate.push_back(quantizer.Quantize(value));
+    }
+    // Score the quantized candidate against the original query.
+    score = scorer->Score(dimension, query.values().data(),
+                          quantized_candidate.data(), quantizer);
+  }
   return embedding_search_range.first <= score &&
          score <= embedding_search_range.second;
 }
@@ -137,7 +159,10 @@
       bool indexable =
           prop->embedding_indexing_config().embedding_indexing_type() !=
           EmbeddingIndexingConfig::EmbeddingIndexingType::UNKNOWN;
-      return PropertyIndexInfo{indexable};
+      EmbeddingIndexingConfig::QuantizationType::Code quantization_type =
+          prop->embedding_indexing_config().quantization_type();
+      return PropertyIndexInfo{indexable, TermMatchType::UNKNOWN,
+                               quantization_type};
     }
 
     if (prop->data_type() != PropertyConfigProto::DataType::DOCUMENT) {
@@ -174,6 +199,14 @@
 InMemoryIcingSearchEngine::DoesDocumentMatchQuery(
     const MonkeyTokenizedDocument &document,
     const SearchSpecProto &search_spec) const {
+  // Check schema type filter.
+  const auto &schema_type_filters = search_spec.schema_type_filters();
+  if (!schema_type_filters.empty() &&
+      std::find(schema_type_filters.begin(), schema_type_filters.end(),
+                document.document.schema()) == schema_type_filters.end()) {
+    return false;
+  }
+
   std::string_view query = search_spec.query();
   std::vector<std::string_view> strs = absl_ports::StrSplit(query, ":");
   std::string_view section_restrict;
@@ -210,6 +243,7 @@
            section.embedding_vectors) {
         if (DoesVectorsMatch(embedding_scorer.get(),
                              embedding_search_range_or.ValueOrDie(),
+                             property_index_info.quantization_type,
                              search_spec.embedding_query_vectors(0), vector)) {
           return true;
         }
@@ -231,7 +265,7 @@
   return false;
 }
 
-void InMemoryIcingSearchEngine::SetSchema(SchemaProto &&schema) {
+void InMemoryIcingSearchEngine::SetSchema(SchemaProto schema) {
   schema_ = std::make_unique<SchemaProto>(std::move(schema));
   property_config_map_.clear();
   for (const SchemaTypeConfigProto &type_config : schema_->types()) {
diff --git a/icing/monkey_test/in-memory-icing-search-engine.h b/icing/monkey_test/in-memory-icing-search-engine.h
index 8e92f10..c8a4b1a 100644
--- a/icing/monkey_test/in-memory-icing-search-engine.h
+++ b/icing/monkey_test/in-memory-icing-search-engine.h
@@ -52,7 +52,7 @@
 
   const SchemaProto *GetSchema() const { return schema_.get(); }
 
-  void SetSchema(SchemaProto &&schema);
+  void SetSchema(SchemaProto schema);
 
   // Randomly pick a document from the in-memory Icing for monkey testing.
   //
@@ -164,6 +164,9 @@
     // The term match type if the property is of type string.
     TermMatchType::Code term_match_type =
         TermMatchType::Code::TermMatchType_Code_UNKNOWN;
+    // The quantization type if the property is of type vector.
+    EmbeddingIndexingConfig::QuantizationType::Code quantization_type =
+        EmbeddingIndexingConfig::QuantizationType::NONE;
   };
   libtextclassifier3::StatusOr<PropertyIndexInfo> GetPropertyIndexInfo(
       const std::string &schema_type,
diff --git a/icing/monkey_test/monkey-test-generators.cc b/icing/monkey_test/monkey-test-generators.cc
index 9f98d15..4dd6a41 100644
--- a/icing/monkey_test/monkey-test-generators.cc
+++ b/icing/monkey_test/monkey-test-generators.cc
@@ -14,6 +14,7 @@
 
 #include "icing/monkey_test/monkey-test-generators.h"
 
+#include <algorithm>
 #include <array>
 #include <cstdint>
 #include <random>
@@ -92,6 +93,10 @@
     property.mutable_embedding_indexing_config()->set_embedding_indexing_type(
         EmbeddingIndexingConfig::EmbeddingIndexingType::LINEAR_SEARCH);
   }
+  if (GetRandomBoolean(random)) {
+    property.mutable_embedding_indexing_config()->set_quantization_type(
+        EmbeddingIndexingConfig::QuantizationType::QUANTIZE_8_BIT);
+  }
 }
 
 }  // namespace
@@ -142,12 +147,34 @@
   return result;
 }
 
+void MonkeySchemaGenerator::ReloadPreviousStatus(const SchemaProto& schema) {
+  int max_schema_id = 0;
+  for (const SchemaTypeConfigProto& type_config : schema.types()) {
+    max_schema_id =
+        std::max(max_schema_id, std::stoi(type_config.schema_type().substr(
+                                    kSchemaTypeNamePrefix.size())));
+
+    // To reset num_properties_generated_ according to the previous run, we use
+    // the maximum property_id + 1 as an estimate.
+    int max_property_id = 0;
+    for (const PropertyConfigProto& property : type_config.properties()) {
+      max_property_id =
+          std::max(max_property_id, std::stoi(property.property_name().substr(
+                                        kSchemaPropertyNamePrefix.size())));
+    }
+    num_properties_generated_[type_config.schema_type()] = max_property_id + 1;
+  }
+  // To reset num_types_generated_ according to the previous run, we use the
+  // maximum schema_id + 1 as an estimate.
+  num_types_generated_ = max_schema_id + 1;
+}
+
 PropertyConfigProto MonkeySchemaGenerator::GenerateProperty(
     const SchemaTypeConfigProto& type_config,
     PropertyConfigProto::Cardinality::Code cardinality, bool indexable) {
   PropertyConfigProto prop;
   prop.set_property_name(
-      "MonkeyTestProp" +
+      std::string(kSchemaPropertyNamePrefix) +
       std::to_string(num_properties_generated_[type_config.schema_type()]++));
   // TODO: Perhaps in future iterations we will want to generate more types of
   // properties.
@@ -201,7 +228,14 @@
       index_incompatible = true;
     }
   } else if (property.data_type() == PropertyConfigProto::DataType::VECTOR) {
+    EmbeddingIndexingConfig::QuantizationType::Code old_quantization_type =
+        property.embedding_indexing_config().quantization_type();
     SetEmbeddingIndexingConfig(random_, property, new_indexable);
+    EmbeddingIndexingConfig::QuantizationType::Code new_quantization_type =
+        property.embedding_indexing_config().quantization_type();
+    if (old_quantization_type != new_quantization_type) {
+      index_incompatible = true;
+    }
   }
   if (index_incompatible) {
     result.schema_types_index_incompatible.insert(type_config.schema_type());
@@ -210,7 +244,7 @@
 
 SchemaTypeConfigProto MonkeySchemaGenerator::GenerateType() {
   SchemaTypeConfigProto type_config;
-  type_config.set_schema_type("MonkeyTestType" +
+  type_config.set_schema_type(std::string(kSchemaTypeNamePrefix) +
                               std::to_string(num_types_generated_++));
   std::uniform_int_distribution<> possible_num_properties_dist(
       0, config_->possible_num_properties.size() - 1);
@@ -325,7 +359,7 @@
     std::uniform_int_distribution<> dist(0, config_->num_uris - 1);
     uri = dist(*random_);
   }
-  return absl_ports::StrCat("uri", std::to_string(uri));
+  return absl_ports::StrCat(kDocumentUriPrefix, std::to_string(uri));
 }
 
 int MonkeyDocumentGenerator::GetNumTokens() const {
diff --git a/icing/monkey_test/monkey-test-generators.h b/icing/monkey_test/monkey-test-generators.h
index 49ad530..5834022 100644
--- a/icing/monkey_test/monkey-test-generators.h
+++ b/icing/monkey_test/monkey-test-generators.h
@@ -35,6 +35,10 @@
 // A random schema generator used for monkey testing.
 class MonkeySchemaGenerator {
  public:
+  static constexpr std::string_view kSchemaTypeNamePrefix = "MonkeyTestType";
+  static constexpr std::string_view kSchemaPropertyNamePrefix =
+      "MonkeyTestProp";
+
   struct UpdateSchemaResult {
     SchemaProto schema;
     bool is_invalid_schema;
@@ -52,6 +56,9 @@
 
   UpdateSchemaResult UpdateSchema(const SchemaProto& schema);
 
+  // Reload the previous status of the schema generator.
+  void ReloadPreviousStatus(const SchemaProto& schema);
+
  private:
   PropertyConfigProto GenerateProperty(
       const SchemaTypeConfigProto& type_config,
@@ -81,6 +88,8 @@
 // Same for num_namespaces.
 class MonkeyDocumentGenerator {
  public:
+  static constexpr std::string_view kDocumentUriPrefix = "uri";
+
   explicit MonkeyDocumentGenerator(
       MonkeyTestRandomEngine* random, const SchemaProto* schema,
       const IcingMonkeyTestRunnerConfiguration* config)
@@ -117,6 +126,13 @@
 
   MonkeyTokenizedDocument GenerateDocument();
 
+  // Reload the previous status of the document generator.
+  void ReloadPreviousStatus(uint32_t max_uri) {
+    // To reset num_docs_generated_ according to the previous run, we use the
+    // maximum uri + 1 as an estimate.
+    num_docs_generated_ = max_uri + 1;
+  }
+
  private:
   MonkeyTestRandomEngine* random_;                    // Does not own.
   const SchemaProto* schema_;                         // Does not own.
diff --git a/icing/monkey_test/monkey-test-util.h b/icing/monkey_test/monkey-test-util.h
index 919640c..2a6f002 100644
--- a/icing/monkey_test/monkey-test-util.h
+++ b/icing/monkey_test/monkey-test-util.h
@@ -31,12 +31,14 @@
 struct IcingMonkeyTestRunnerConfiguration {
   explicit IcingMonkeyTestRunnerConfiguration(uint32_t seed, int num_types,
                                               int num_namespaces, int num_uris,
-                                              int index_merge_size)
+                                              int index_merge_size,
+                                              bool initialize_by_existing_data)
       : seed(seed),
         num_types(num_types),
         num_namespaces(num_namespaces),
         num_uris(num_uris),
-        index_merge_size(index_merge_size) {}
+        index_merge_size(index_merge_size),
+        initialize_by_existing_data(initialize_by_existing_data) {}
 
   uint32_t seed;
   int num_types;
@@ -44,6 +46,12 @@
   int num_uris;
   int index_merge_size;
 
+  // Whether to initialize Icing with existing data. If true, the test will
+  // start from the state of the existing Icing testing data stored in
+  // GetTestTempDir() + "/icing/monkey". Otherwise, the test will start with an
+  // empty Icing.
+  bool initialize_by_existing_data;
+
   // To ensure that the random schema is generated with the best quality, the
   // number of properties for each type will only be randomly picked from this
   // list, instead of picking it from a range. For example, a vector of
diff --git a/icing/monkey_test/monkey-tokenized-document.h b/icing/monkey_test/monkey-tokenized-document.h
index cad838e..ce356c1 100644
--- a/icing/monkey_test/monkey-tokenized-document.h
+++ b/icing/monkey_test/monkey-tokenized-document.h
@@ -16,13 +16,21 @@
 #define ICING_MONKEY_TEST_MONKEY_TOKENIZED_DOCUMENT_H_
 
 #include <string>
+#include <string_view>
+#include <utility>
 #include <vector>
 
+#include "icing/absl_ports/str_cat.h"
+#include "icing/absl_ports/str_join.h"
 #include "icing/proto/document.pb.h"
 
 namespace icing {
 namespace lib {
 
+// No matter whether the property is indexable, we have to create a section for
+// it since a non-indexable property can become indexable after a schema type
+// change. The in-memory icing will automatically skip sections that are
+// non-indexable at the time of search requests.
 struct MonkeyTokenizedSection {
   std::string path;
   std::vector<std::string> token_sequence;
@@ -32,6 +40,50 @@
 struct MonkeyTokenizedDocument {
   DocumentProto document;
   std::vector<MonkeyTokenizedSection> tokenized_sections;
+
+  static MonkeyTokenizedDocument Reload(DocumentProto document) {
+    MonkeyTokenizedDocument tokenized_document;
+    tokenized_document.document = std::move(document);
+    ExtractTokenizedSections(tokenized_document.document, /*curr_path=*/"",
+                             tokenized_document.tokenized_sections);
+    return tokenized_document;
+  }
+
+ private:
+  static void ExtractTokenizedSections(
+      const DocumentProto& document, std::string curr_path,
+      std::vector<MonkeyTokenizedSection>& tokenized_sections) {
+    for (const PropertyProto& property : document.properties()) {
+      std::string new_path =
+          curr_path.empty()
+              ? property.name()
+              : absl_ports::StrCat(curr_path, ".", property.name());
+      if (property.string_values_size() > 0) {
+        std::vector<std::string> token_sequence;
+        for (const std::string& value : property.string_values()) {
+          for (std::string_view token : absl_ports::StrSplit(value, " ")) {
+            token_sequence.push_back(std::string(token));
+          }
+        }
+        tokenized_sections.push_back(MonkeyTokenizedSection{
+            .path = new_path, .token_sequence = std::move(token_sequence)});
+      } else if (property.vector_values_size() > 0) {
+        std::vector<PropertyProto::VectorProto> embedding_vectors;
+        for (const PropertyProto::VectorProto& vector :
+             property.vector_values()) {
+          embedding_vectors.push_back(vector);
+        }
+        tokenized_sections.push_back(MonkeyTokenizedSection{
+            .path = new_path,
+            .embedding_vectors = std::move(embedding_vectors)});
+      } else if (property.document_values_size() > 0) {
+        for (const DocumentProto& document_value : property.document_values()) {
+          ExtractTokenizedSections(document_value, new_path,
+                                   tokenized_sections);
+        }
+      }
+    }
+  }
 };
 
 }  // namespace lib
diff --git a/icing/portable/gzip_stream.cc b/icing/portable/gzip_stream.cc
index f00a993..ea5094a 100644
--- a/icing/portable/gzip_stream.cc
+++ b/icing/portable/gzip_stream.cc
@@ -18,17 +18,23 @@
 // smaller libprotobuf-lite instead.
 
 #include "icing/portable/gzip_stream.h"
+
+#include <cstddef>
+
 #include "icing/util/logging.h"
 
 namespace icing {
 namespace lib {
 namespace protobuf_ports {
 
-static const int kDefaultBufferSize = 65536;
-
 GzipInputStream::GzipInputStream(ZeroCopyInputStream* sub_stream, Format format,
-                                 int buffer_size)
-    : format_(format), sub_stream_(sub_stream), zerror_(Z_OK), byte_count_(0) {
+                                 void* output_buffer, size_t buffer_size)
+    : format_(format),
+      sub_stream_(sub_stream),
+      zerror_(Z_OK),
+      output_buffer_(output_buffer),
+      output_buffer_length_(buffer_size),
+      byte_count_(0) {
   zcontext_.state = Z_NULL;
   zcontext_.zalloc = Z_NULL;
   zcontext_.zfree = Z_NULL;
@@ -38,18 +44,12 @@
   zcontext_.avail_in = 0;
   zcontext_.total_in = 0;
   zcontext_.msg = NULL;
-  if (buffer_size == -1) {
-    output_buffer_length_ = kDefaultBufferSize;
-  } else {
-    output_buffer_length_ = buffer_size;
-  }
-  output_buffer_ = operator new(output_buffer_length_);
   zcontext_.next_out = static_cast<Bytef*>(output_buffer_);
   zcontext_.avail_out = output_buffer_length_;
   output_position_ = output_buffer_;
 }
+
 GzipInputStream::~GzipInputStream() {
-  operator delete(output_buffer_);
   zerror_ = inflateEnd(&zcontext_);
 }
 
@@ -179,7 +179,8 @@
     : format(GZIP),
       buffer_size(kDefaultBufferSize),
       compression_level(Z_DEFAULT_COMPRESSION),
-      compression_strategy(Z_DEFAULT_STRATEGY) {}
+      compression_strategy(Z_DEFAULT_STRATEGY),
+      mem_level(kDefaultMemLevel) {}
 
 GzipOutputStream::GzipOutputStream(ZeroCopyOutputStream* sub_stream) {
   Init(sub_stream, Options());
@@ -214,10 +215,10 @@
   if (options.format == ZLIB) {
     windowBitsFormat = 0;
   }
-  zerror_ =
-      deflateInit2(&zcontext_, options.compression_level, Z_DEFLATED,
-                   /* windowBits */ 15 | windowBitsFormat,
-                   /* memLevel (default) */ 8, options.compression_strategy);
+  zerror_ = deflateInit2(&zcontext_, options.compression_level, Z_DEFLATED,
+                         /* windowBits */ 15 | windowBitsFormat,
+                         /* memLevel */ options.mem_level,
+                         options.compression_strategy);
 }
 
 GzipOutputStream::~GzipOutputStream() {
diff --git a/icing/portable/gzip_stream.h b/icing/portable/gzip_stream.h
index 8008a55..a43f9e2 100644
--- a/icing/portable/gzip_stream.h
+++ b/icing/portable/gzip_stream.h
@@ -27,6 +27,9 @@
 #ifndef GOOGLE3_ICING_PORTABLE_GZIP_STREAM_H_
 #define GOOGLE3_ICING_PORTABLE_GZIP_STREAM_H_
 
+#include <cstddef>
+#include <cstdint>
+
 #include "icing/portable/zlib.h"
 #include <google/protobuf/io/zero_copy_stream_impl_lite.h>
 
@@ -34,6 +37,9 @@
 namespace lib {
 namespace protobuf_ports {
 
+static constexpr size_t kDefaultBufferSize = 64 * 1024;  // 64kb
+static constexpr int kDefaultMemLevel = 8;
+
 // A ZeroCopyInputStream that reads compressed data through zlib
 class GzipInputStream : public google::protobuf::io::ZeroCopyInputStream {
  public:
@@ -51,7 +57,8 @@
 
   // buffer_size and format may be -1 for default of 64kB and GZIP format
   explicit GzipInputStream(google::protobuf::io::ZeroCopyInputStream* sub_stream,
-                           Format format = AUTO, int buffer_size = -1);
+                           Format format, void* output_buffer,
+                           size_t buffer_size);
   virtual ~GzipInputStream();
 
   // Return last error message or NULL if no error.
@@ -67,12 +74,12 @@
  private:
   Format format_;
 
-  google::protobuf::io::ZeroCopyInputStream* sub_stream_;
+  google::protobuf::io::ZeroCopyInputStream* sub_stream_;  // Does not own.
 
   z_stream zcontext_;
   int zerror_;
 
-  void* output_buffer_;
+  void* output_buffer_;  // Does not own.
   void* output_position_;
   size_t output_buffer_length_;
   int64_t byte_count_;
@@ -108,6 +115,16 @@
     // zlib.h for definitions of these constants.
     int compression_strategy;
 
+    // Used for deflateInit2.
+    // A number between 1 and 9, specifying how much memory should be allocated
+    // for the internal compression state.
+    // memLevel=1 uses minimum memory but is slow and reduces compression ratio;
+    // memLevel=9 uses maximum memory for optimal speed.
+    // The default value is 8.
+    //
+    // See the documentation for deflateInit2 in zlib.h for more details.
+    int mem_level;
+
     Options();  // Initializes with default values.
   };
 
diff --git a/icing/query/advanced_query_parser/abstract-syntax-tree.h b/icing/query/advanced_query_parser/abstract-syntax-tree.h
index 3260084..071dd30 100644
--- a/icing/query/advanced_query_parser/abstract-syntax-tree.h
+++ b/icing/query/advanced_query_parser/abstract-syntax-tree.h
@@ -99,6 +99,26 @@
   bool is_prefix_;
 };
 
+class FunctionNode : public Node {
+ public:
+  explicit FunctionNode(std::string function_name)
+      : FunctionNode(std::move(function_name), {}) {}
+  explicit FunctionNode(std::string function_name,
+                        std::vector<std::unique_ptr<Node>> args)
+      : function_name_(std::move(function_name)),
+        args_(std::move(args)) {}
+
+  void Accept(AbstractSyntaxTreeVisitor* visitor) const override {
+    visitor->VisitFunction(this);
+  }
+  const std::string& function_name() const { return function_name_; }
+  const std::vector<std::unique_ptr<Node>>& args() const { return args_; }
+
+ private:
+  std::string function_name_;
+  std::vector<std::unique_ptr<Node>> args_;
+};
+
 class MemberNode : public Node {
  public:
   explicit MemberNode(std::vector<std::unique_ptr<TextNode>> children,
@@ -120,26 +140,6 @@
   std::unique_ptr<FunctionNode> function_;
 };
 
-class FunctionNode : public Node {
- public:
-  explicit FunctionNode(std::string function_name)
-      : FunctionNode(std::move(function_name), {}) {}
-  explicit FunctionNode(std::string function_name,
-                        std::vector<std::unique_ptr<Node>> args)
-      : function_name_(std::move(function_name)),
-        args_(std::move(args)) {}
-
-  void Accept(AbstractSyntaxTreeVisitor* visitor) const override {
-    visitor->VisitFunction(this);
-  }
-  const std::string& function_name() const { return function_name_; }
-  const std::vector<std::unique_ptr<Node>>& args() const { return args_; }
-
- private:
-  std::string function_name_;
-  std::vector<std::unique_ptr<Node>> args_;
-};
-
 class UnaryOperatorNode : public Node {
  public:
   explicit UnaryOperatorNode(std::string operator_text,
diff --git a/icing/query/advanced_query_parser/optimizer/query-optimization-util.cc b/icing/query/advanced_query_parser/optimizer/query-optimization-util.cc
new file mode 100644
index 0000000..6abc4b3
--- /dev/null
+++ b/icing/query/advanced_query_parser/optimizer/query-optimization-util.cc
@@ -0,0 +1,68 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/query/advanced_query_parser/optimizer/query-optimization-util.h"
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "icing/feature-flags.h"
+#include "icing/index/embed/doc-hit-info-iterator-embedding-v2.h"
+#include "icing/index/iterator/doc-hit-info-iterator-and.h"
+#include "icing/index/iterator/doc-hit-info-iterator.h"
+
+namespace icing {
+namespace lib {
+namespace query_optimization_util {
+
+std::unique_ptr<DocHitInfoIterator> OptimizeAndIteratorsIfPossible(
+    std::vector<std::unique_ptr<DocHitInfoIterator>>&& iterators,
+    const FeatureFlags& feature_flags) {
+  std::unique_ptr<DocHitInfoIterator> embed_iterator;
+  bool delegate_node_is_right_most = true;
+  if (feature_flags.enable_embed_query_optimization()) {
+    // Find the first embedding iterator and remove it from the vector.
+    int embed_iterator_index = 0;
+    for (int i = embed_iterator_index; embed_iterator_index < iterators.size();
+         ++embed_iterator_index) {
+      if (iterators.at(i)->CanAdoptDelegate()) {
+        embed_iterator = std::move(iterators.at(i));
+        if (i == iterators.size() - 1) {
+          // If this embedding iterator is the last iterator, then the node is
+          // the right most node and the delegate that we're going to create
+          // would not be right most.
+          delegate_node_is_right_most = false;
+        }
+        iterators.erase(iterators.begin() + i);
+        break;
+      }
+    }
+  }
+  // If we found an embedding iterator, then put all other iterators into an
+  // AND iterator together and pass it to this embedding iterator as a delegate.
+  // Otherwise, just return the original AND iterator.
+  std::unique_ptr<DocHitInfoIterator> and_iterator =
+      CreateAndIterator(std::move(iterators));
+  if (embed_iterator == nullptr) {
+    return and_iterator;
+  }
+  embed_iterator->AdoptDelegate(std::move(and_iterator),
+                                    delegate_node_is_right_most);
+  return embed_iterator;
+}
+
+}  // namespace query_optimization_util
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/query/advanced_query_parser/optimizer/query-optimization-util.h b/icing/query/advanced_query_parser/optimizer/query-optimization-util.h
new file mode 100644
index 0000000..c24a97c
--- /dev/null
+++ b/icing/query/advanced_query_parser/optimizer/query-optimization-util.h
@@ -0,0 +1,40 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef ICING_QUERY_ADVANCED_QUERY_PARSER_OPTIMIZER_QUERY_OPTIMIZATION_UTIL_H_
+#define ICING_QUERY_ADVANCED_QUERY_PARSER_OPTIMIZER_QUERY_OPTIMIZATION_UTIL_H_
+
+#include <memory>
+#include <vector>
+
+#include "icing/feature-flags.h"
+#include "icing/index/iterator/doc-hit-info-iterator.h"
+
+namespace icing {
+namespace lib {
+namespace query_optimization_util {
+
+// Optimizes the given iterators that are all intended to be AND'd together.
+// If an optimization is possible, then this node will be rewritten to an
+// equivalent but more efficient iterator sub-tree. Otherwise, the iterators
+// will be returned as a normal AND iterator.
+std::unique_ptr<DocHitInfoIterator> OptimizeAndIteratorsIfPossible(
+    std::vector<std::unique_ptr<DocHitInfoIterator>>&& iterators,
+    const FeatureFlags& feature_flags);
+
+}  // namespace query_optimization_util
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_QUERY_ADVANCED_QUERY_PARSER_OPTIMIZER_QUERY_OPTIMIZATION_UTIL_H_
diff --git a/icing/query/advanced_query_parser/query-visitor.cc b/icing/query/advanced_query_parser/query-visitor.cc
index 04624b7..492a416 100644
--- a/icing/query/advanced_query_parser/query-visitor.cc
+++ b/icing/query/advanced_query_parser/query-visitor.cc
@@ -33,7 +33,8 @@
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/absl_ports/str_cat.h"
 #include "icing/absl_ports/str_join.h"
-#include "icing/index/embed/doc-hit-info-iterator-embedding.h"
+#include "icing/index/embed/doc-hit-info-iterator-embedding-v1.h"
+#include "icing/index/embed/doc-hit-info-iterator-embedding-v2.h"
 #include "icing/index/embed/embedding-query-results.h"
 #include "icing/index/iterator/doc-hit-info-iterator-all-document-id.h"
 #include "icing/index/iterator/doc-hit-info-iterator-and.h"
@@ -50,6 +51,7 @@
 #include "icing/query/advanced_query_parser/abstract-syntax-tree.h"
 #include "icing/query/advanced_query_parser/function.h"
 #include "icing/query/advanced_query_parser/lexer.h"
+#include "icing/query/advanced_query_parser/optimizer/query-optimization-util.h"
 #include "icing/query/advanced_query_parser/param.h"
 #include "icing/query/advanced_query_parser/parser.h"
 #include "icing/query/advanced_query_parser/pending-value.h"
@@ -200,9 +202,9 @@
                              search_spec_.term_match_type(),
                              needs_term_frequency_info_));
       query_term_iterators_[query_term.term] =
-          std::make_unique<DocHitInfoIteratorFilter>(
-              std::move(term_iterator), &document_store_, &schema_store_,
-              filter_options_, current_time_ms_);
+          DocHitInfoIteratorFilter::ApplyFilter(
+              std::move(term_iterator), filter_predicate_,
+              feature_flags_.enable_passing_filter_to_children());
     }
   }
 
@@ -351,7 +353,7 @@
     QueryVisitor query_visitor(
         &index_, &numeric_index_, &embedding_index_, &document_store_,
         &schema_store_, &normalizer_, &tokenizer_, join_children_fetcher_,
-        search_spec_, filter_options_, needs_term_frequency_info_,
+        search_spec_, filter_predicate_, needs_term_frequency_info_,
         get_embedding_match_info_, &feature_flags_, pending_property_restricts_,
         processing_not_, current_time_ms_);
     tree_root->Accept(&query_visitor);
@@ -462,14 +464,34 @@
   }
 
   // Create and return iterator.
-  EmbeddingQueryResults::EmbeddingQueryMatchInfoMap* info_map =
-      &embedding_query_results_.result_infos[vector_index][metric_type];
   ICING_ASSIGN_OR_RETURN(
-      std::unique_ptr<DocHitInfoIterator> iterator,
-      DocHitInfoIteratorEmbedding::Create(
-          &search_spec_.embedding_query_vectors(vector_index), metric_type, low,
-          high, get_embedding_match_info_, info_map, &embedding_index_,
-          &document_store_, &schema_store_, current_time_ms_));
+      EmbeddingQueryResults::EmbeddingQueryMatchInfoMap * info_map,
+      embedding_query_results_.GetOrCreateMatchInfoMap(vector_index,
+                                                       metric_type));
+  std::unique_ptr<DocHitInfoIterator> iterator;
+  if (feature_flags_.enable_embedding_iterator_v2()) {
+    ICING_ASSIGN_OR_RETURN(
+        iterator,
+        DocHitInfoIteratorEmbeddingV2::Create(
+            &search_spec_.embedding_query_vectors(vector_index), metric_type,
+            low, high, info_map, embedding_query_results_.global_scores.get(),
+            get_embedding_match_info_
+                ? embedding_query_results_.global_section_infos.get()
+                : nullptr,
+            &embedding_index_, &document_store_, &schema_store_,
+            current_time_ms_));
+  } else {
+    ICING_ASSIGN_OR_RETURN(
+        iterator,
+        DocHitInfoIteratorEmbeddingV1::Create(
+            &search_spec_.embedding_query_vectors(vector_index), metric_type,
+            low, high, info_map, embedding_query_results_.global_scores.get(),
+            get_embedding_match_info_
+                ? embedding_query_results_.global_section_infos.get()
+                : nullptr,
+            &embedding_index_, &document_store_, &schema_store_,
+            current_time_ms_));
+  }
   return PendingValue(std::move(iterator));
 }
 
@@ -565,15 +587,16 @@
   std::vector<std::unique_ptr<DocHitInfoIterator>> iterators;
   // raw_text is the portion of text_value.raw_term that hasn't yet been
   // matched to any of the tokens that we've processed. escaped_token will
-  // hold the portion of raw_text that corresponds to the current token that
+  // holds the portion of raw_text that corresponds to the current token that
   // is being processed.
   std::string_view raw_text = text_value.raw_term;
+  std::vector<Token> tokens;
   std::string_view raw_token;
   bool reached_final_token = !token_itr->Advance();
   // If the term is different then the raw_term, then there must have been some
   // escaped characters that we will need to handle.
   while (!reached_final_token) {
-    std::vector<Token> tokens = token_itr->GetTokens();
+    token_itr->GetTokens(&tokens);
     if (tokens.size() > 1) {
       // The tokenizer iterator iterates between token groups. In practice,
       // the tokenizer used with QueryVisitor (PlainTokenizer) will always
@@ -717,7 +740,8 @@
   ICING_ASSIGN_OR_RETURN(
       std::vector<std::unique_ptr<DocHitInfoIterator>> iterators,
       PopAllPendingIterators());
-  return PendingValue(CreateAndIterator(std::move(iterators)));
+  return PendingValue(query_optimization_util::OptimizeAndIteratorsIfPossible(
+      std::move(iterators), feature_flags_));
 }
 
 libtextclassifier3::StatusOr<PendingValue> QueryVisitor::ProcessOrOperator(
diff --git a/icing/query/advanced_query_parser/query-visitor.h b/icing/query/advanced_query_parser/query-visitor.h
index 746d921..b1fd3c6 100644
--- a/icing/query/advanced_query_parser/query-visitor.h
+++ b/icing/query/advanced_query_parser/query-visitor.h
@@ -31,8 +31,8 @@
 #include "icing/index/embed/embedding-index.h"
 #include "icing/index/embed/embedding-query-results.h"
 #include "icing/index/index.h"
-#include "icing/index/iterator/doc-hit-info-iterator-filter.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/index/iterator/document-filter-predicate.h"
 #include "icing/index/numeric/numeric-index.h"
 #include "icing/join/join-children-fetcher.h"
 #include "icing/query/advanced_query_parser/abstract-syntax-tree.h"
@@ -60,12 +60,12 @@
       const Normalizer* normalizer, const Tokenizer* tokenizer,
       const JoinChildrenFetcher* join_children_fetcher,
       const SearchSpecProto& search_spec,
-      DocHitInfoIteratorFilter::Options filter_options,
+      const DocumentFilterPredicate* filter_predicate,
       bool needs_term_frequency_info, bool get_embedding_match_info,
       const FeatureFlags* feature_flags, int64_t current_time_ms)
       : QueryVisitor(index, numeric_index, embedding_index, document_store,
                      schema_store, normalizer, tokenizer, join_children_fetcher,
-                     search_spec, filter_options, needs_term_frequency_info,
+                     search_spec, filter_predicate, needs_term_frequency_info,
                      get_embedding_match_info, feature_flags,
                      PendingPropertyRestricts(),
                      /*processing_not=*/false, current_time_ms) {}
@@ -119,12 +119,13 @@
       const Normalizer* normalizer, const Tokenizer* tokenizer,
       const JoinChildrenFetcher* join_children_fetcher,
       const SearchSpecProto& search_spec,
-      DocHitInfoIteratorFilter::Options filter_options,
+      const DocumentFilterPredicate* filter_predicate,
       bool needs_term_frequency_info, bool get_embedding_match_info,
       const FeatureFlags* feature_flags,
       PendingPropertyRestricts pending_property_restricts, bool processing_not,
       int64_t current_time_ms)
-      : index_(*index),
+      : embedding_query_results_(search_spec.embedding_query_vectors_size()),
+        index_(*index),
         numeric_index_(*numeric_index),
         embedding_index_(*embedding_index),
         document_store_(*document_store),
@@ -133,7 +134,7 @@
         tokenizer_(*tokenizer),
         join_children_fetcher_(join_children_fetcher),
         search_spec_(search_spec),
-        filter_options_(std::move(filter_options)),
+        filter_predicate_(filter_predicate),
         needs_term_frequency_info_(needs_term_frequency_info),
         get_embedding_match_info_(get_embedding_match_info),
         feature_flags_(*feature_flags),
@@ -381,9 +382,9 @@
   // parent query for a join query, in which case child scores are available.
   const JoinChildrenFetcher* join_children_fetcher_;  // Does not own.
 
-  const SearchSpecProto& search_spec_;
+  const SearchSpecProto& search_spec_;  // Does not own.
 
-  DocHitInfoIteratorFilter::Options filter_options_;
+  const DocumentFilterPredicate* filter_predicate_;  // Does not own.
 
   // Whether or not term_frequency information is needed. This affects:
   //  - how DocHitInfoIteratorTerms are constructed
diff --git a/icing/query/advanced_query_parser/query-visitor_test.cc b/icing/query/advanced_query_parser/query-visitor_test.cc
index 3c50b4a..3d1a903 100644
--- a/icing/query/advanced_query_parser/query-visitor_test.cc
+++ b/icing/query/advanced_query_parser/query-visitor_test.cc
@@ -39,14 +39,15 @@
 #include "icing/index/embed/embedding-query-results.h"
 #include "icing/index/hit/hit.h"
 #include "icing/index/index.h"
-#include "icing/index/iterator/doc-hit-info-iterator-filter.h"
 #include "icing/index/iterator/doc-hit-info-iterator-test-util.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/index/iterator/document-filter-predicate.h"
 #include "icing/index/numeric/dummy-numeric-index.h"
 #include "icing/index/numeric/numeric-index.h"
 #include "icing/index/property-existence-indexing-handler.h"
 #include "icing/jni/jni-cache.h"
 #include "icing/legacy/index/icing-filesystem.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/search.pb.h"
 #include "icing/query/advanced_query_parser/abstract-syntax-tree.h"
@@ -54,9 +55,11 @@
 #include "icing/query/advanced_query_parser/parser.h"
 #include "icing/query/query-features.h"
 #include "icing/query/query-results.h"
+#include "icing/query/query-utils.h"
 #include "icing/schema-builder.h"
 #include "icing/schema/schema-store.h"
 #include "icing/schema/section.h"
+#include "icing/scoring/advanced_scoring/double-list.h"
 #include "icing/store/document-id.h"
 #include "icing/store/document-store.h"
 #include "icing/store/namespace-id.h"
@@ -74,6 +77,7 @@
 #include "icing/transform/normalizer-options.h"
 #include "icing/transform/normalizer.h"
 #include "icing/util/clock.h"
+#include "icing/util/document-util.h"
 #include "icing/util/icu-data-file-helper.h"
 #include "icing/util/status-macros.h"
 #include "unicode/uloc.h"
@@ -88,7 +92,6 @@
 using ::testing::ElementsAre;
 using ::testing::IsEmpty;
 using ::testing::IsNull;
-using ::testing::Pointee;
 using ::testing::UnorderedElementsAre;
 
 constexpr float kEps = 0.000001;
@@ -165,20 +168,25 @@
       /*embedding_query_vectors=*/{}, EMBEDDING_METRIC_UNKNOWN);
 }
 
-bool ContainsMatchInfoEntry(const EmbeddingMatchInfos* match_info, double score,
+bool ContainsMatchInfoEntry(EmbeddingQueryResults& embedding_query_results,
+                            const EmbeddingMatchInfos* match_info, double score,
                             int position_in_section, SectionId section_id) {
-  if (match_info == nullptr || match_info->section_infos == nullptr ||
-      match_info->scores.empty()) {
-    return false;
-  }
-  if (match_info->scores.size() != match_info->section_infos->size()) {
+  if (match_info == nullptr ||
+      embedding_query_results.global_section_infos->empty()) {
     return false;
   }
 
-  for (int i = 0; i < match_info->scores.size(); ++i) {
-    if (std::fabs(match_info->scores[i] - score) < kEps &&
-        match_info->section_infos->at(i).position == position_in_section &&
-        match_info->section_infos->at(i).section_id == section_id) {
+  DoubleList matched_scores =
+      embedding_query_results.GetMatchedScoresFromEmbeddingMatchInfos(
+          *match_info);
+
+  for (int i = 0; i < matched_scores.size(); ++i) {
+    const EmbeddingMatchInfos::EmbeddingMatchSectionInfo& section_info =
+        embedding_query_results.global_section_infos->at(
+            i + match_info->score_start_index);
+    if (std::fabs(matched_scores.data()[i] - score) < kEps &&
+        section_info.position == position_in_section &&
+        section_info.section_id == section_id) {
       return true;
     }
   }
@@ -193,18 +201,45 @@
 struct QueryVisitorTestParams {
   QueryType query_type;
   bool get_embedding_match_info;
+  bool enable_embedding_iterator_v2;
+  bool enable_embed_query_optimization;
 
   explicit QueryVisitorTestParams(QueryType query_type,
-                                  bool get_embedding_match_info)
+                                  bool get_embedding_match_info,
+                                  bool enable_embedding_iterator_v2,
+                                  bool enable_embed_query_optimization)
       : query_type(query_type),
-        get_embedding_match_info(get_embedding_match_info) {}
+        get_embedding_match_info(get_embedding_match_info),
+        enable_embedding_iterator_v2(enable_embedding_iterator_v2),
+        enable_embed_query_optimization(enable_embed_query_optimization) {}
 };
 
 class QueryVisitorTest
     : public ::testing::TestWithParam<QueryVisitorTestParams> {
  protected:
   void SetUp() override {
-    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+    feature_flags_ = std::make_unique<FeatureFlags>(
+        FeatureFlags(/*enable_circular_schema_definitions=*/true,
+                     /*enable_scorable_properties=*/true,
+                     /*enable_embedding_quantization=*/true,
+                     /*enable_repeated_field_joins=*/true,
+                     /*enable_embedding_backup_generation=*/true,
+                     /*enable_schema_database=*/true,
+                     /*release_backup_schema_file_if_overlay_present=*/true,
+                     /*enable_strict_page_byte_size_limit=*/true,
+                     /*enable_smaller_decompression_buffer_size=*/true,
+                     /*enable_eigen_embedding_scoring=*/true,
+                     /*enable_passing_filter_to_children=*/true,
+                     /*enable_proto_log_new_header_format=*/true,
+                     GetParam().enable_embedding_iterator_v2,
+                     /*enable_reusable_decompression_buffer=*/true,
+                     /*enable_schema_type_id_optimization=*/true,
+                     /*enable_optimize_improvements=*/true,
+                     /*expired_document_purge_threshold_ms=*/0,
+                     /*enable_non_existent_qualified_id_join=*/true,
+                     /*enable_skip_set_schema_type_equality_check=*/true,
+                     /*enable_embed_query_optimization=*/true,
+                     /*enable_schema_definition_deduping=*/true));
     test_dir_ = GetTestTempDir() + "/icing";
     index_dir_ = test_dir_ + "/index";
     numeric_index_dir_ = test_dir_ + "/numeric_index";
@@ -236,14 +271,18 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, store_dir_, &clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, store_dir_, &clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
 
     Index::Options options(index_dir_.c_str(),
@@ -251,7 +290,8 @@
                            /*lite_index_sort_at_indexing=*/true,
                            /*lite_index_sort_size=*/1024 * 8);
     ICING_ASSERT_OK_AND_ASSIGN(
-        index_, Index::Create(options, &filesystem_, &icing_filesystem_));
+        index_, Index::Create(options, &filesystem_, &icing_filesystem_,
+                              feature_flags_.get()));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         numeric_index_,
@@ -260,7 +300,8 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         embedding_index_,
         EmbeddingIndex::Create(&filesystem_, embedding_index_dir_, &clock_,
-                               feature_flags_.get()));
+                               feature_flags_.get(),
+                               /*num_shards=*/32));
 
     NormalizerOptions normalizer_options(
         /*max_term_byte_size=*/std::numeric_limits<int32_t>::max());
@@ -277,6 +318,10 @@
                                tokenizer_factory::CreateIndexingTokenizer(
                                    StringIndexingConfig::TokenizerType::PLAIN,
                                    language_segmenter_.get()));
+
+    document_filter_predicate_ = GetFilterPredicateBySchemaAndNamespace(
+        SearchSpecProto::default_instance(), *document_store_, *schema_store_,
+        clock_.GetSystemTimeMilliseconds());
   }
 
   libtextclassifier3::StatusOr<std::unique_ptr<Node>> ParseQueryHelper(
@@ -295,7 +340,7 @@
         index_.get(), numeric_index_.get(), embedding_index_.get(),
         document_store_.get(), schema_store_.get(), normalizer_.get(),
         tokenizer_.get(), /*join_children_fetcher=*/nullptr, search_spec,
-        DocHitInfoIteratorFilter::Options(),
+        document_filter_predicate_.get(),
         /*needs_term_frequency_info=*/true, get_embedding_match_info,
         feature_flags_.get(), clock_.GetSystemTimeMilliseconds());
     root_node->Accept(&query_visitor);
@@ -364,6 +409,7 @@
   std::unique_ptr<Normalizer> normalizer_;
   std::unique_ptr<LanguageSegmenter> language_segmenter_;
   std::unique_ptr<Tokenizer> tokenizer_;
+  std::unique_ptr<DocumentFilterPredicate> document_filter_predicate_;
   std::unique_ptr<const JniCache> jni_cache_;
 };
 
@@ -769,7 +815,7 @@
       index_.get(), numeric_index_.get(), embedding_index_.get(),
       document_store_.get(), schema_store_.get(), normalizer_.get(),
       tokenizer_.get(), /*join_children_fetcher=*/nullptr, search_spec,
-      DocHitInfoIteratorFilter::Options(),
+      document_filter_predicate_.get(),
       /*needs_term_frequency_info=*/true, /*get_embedding_match_info=*/false,
       feature_flags_.get(), clock_.GetSystemTimeMilliseconds());
   EXPECT_THAT(std::move(query_visitor).ConsumeResults(),
@@ -838,8 +884,8 @@
   // - Doc0: ["-2", "-1", "1", "2"] and [-2, -1, 1, 2]
   // - Doc1: [-1]
   // - Doc2: ["2"] and [-1]
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
   std::unique_ptr<NumericIndex<int64_t>::Editor> editor =
       numeric_index_->Edit("price", kDocumentId0, kSectionId0);
   ICING_ASSERT_OK(editor->BufferKey(-2));
@@ -855,14 +901,14 @@
   ICING_ASSERT_OK(term_editor.BufferTerm("2", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(term_editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   editor = numeric_index_->Edit("price", kDocumentId1, kSectionId0);
   ICING_ASSERT_OK(editor->BufferKey(-1));
   ICING_ASSERT_OK(std::move(*editor).IndexAllBufferedKeys());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
   editor = numeric_index_->Edit("price", kDocumentId2, kSectionId0);
   ICING_ASSERT_OK(editor->BufferKey(-1));
   ICING_ASSERT_OK(std::move(*editor).IndexAllBufferedKeys());
@@ -966,7 +1012,7 @@
       index_.get(), numeric_index_.get(), embedding_index_.get(),
       document_store_.get(), schema_store_.get(), normalizer_.get(),
       tokenizer_.get(), /*join_children_fetcher=*/nullptr, search_spec,
-      DocHitInfoIteratorFilter::Options(),
+      document_filter_predicate_.get(),
       /*needs_term_frequency_info=*/false, /*get_embedding_match_info=*/false,
       feature_flags_.get(), clock_.GetSystemTimeMilliseconds());
   root_node->Accept(&query_visitor);
@@ -1455,22 +1501,22 @@
           .Build(),
       /*ignore_errors_and_delete_documents=*/false));
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
@@ -1499,22 +1545,22 @@
           .Build(),
       /*ignore_errors_and_delete_documents=*/false));
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
@@ -1539,8 +1585,8 @@
           .Build(),
       /*ignore_errors_and_delete_documents=*/false));
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -1548,16 +1594,16 @@
   ICING_ASSERT_OK(editor.BufferTerm("baz", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.BufferTerm("baz", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
@@ -1587,8 +1633,8 @@
           .Build(),
       /*ignore_errors_and_delete_documents=*/false));
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -1596,16 +1642,16 @@
   ICING_ASSERT_OK(editor.BufferTerm("baz", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.BufferTerm("baz", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
@@ -1819,23 +1865,23 @@
           .Build(),
       /*ignore_errors_and_delete_documents=*/false));
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -1889,22 +1935,22 @@
   SectionId prop1_section_id = 0;
   SectionId prop2_section_id = 1;
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId2, prop2_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -1955,22 +2001,22 @@
   SectionId prop2_section_id = 1;
   SectionId prop3_section_id = 2;
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId1, prop2_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId2, prop3_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -2035,22 +2081,22 @@
   SectionId prop1_section_id = 0;
   SectionId prop2_section_id = 1;
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId2, prop2_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -2095,22 +2141,22 @@
   SectionId prop1_section_id = 0;
   SectionId prop2_section_id = 1;
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId2, prop2_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -2153,22 +2199,22 @@
   SectionId prop1_section_id = 0;
   SectionId prop2_section_id = 1;
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId2, prop2_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -2222,22 +2268,22 @@
   SectionId prop1_section_id = 0;
   SectionId prop2_section_id = 1;
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId2, prop2_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -2287,22 +2333,22 @@
   SectionId prop1_section_id = 0;
   SectionId prop2_section_id = 1;
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId2, prop2_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -2367,22 +2413,22 @@
   //   Doc2:
   //     prop1: ""
   //     prop2: "foo"
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId2, prop2_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -2452,8 +2498,8 @@
   // ICU segmentation will break this into "每天" and "上班".
   // CFStringTokenizer (ios) will break this into "每", "天" and "上班"
   std::string query = CreateQuery("每天上班");
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("上班", TERM_MATCH_PREFIX));
@@ -2468,15 +2514,15 @@
   }
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("上班", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId2, prop2_section_id,
                         /*namespace_id=*/0);
   if (IsCfStringTokenization()) {
@@ -2537,8 +2583,9 @@
   // - Doc 0: Contains 'val0', 'val1', 'val2' in 'prop0'. Shouldn't match.
   DocumentProto doc =
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(doc));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(document_util::CreateDocumentWrapper(doc)));
   DocumentId docid0 = put_result0.new_document_id;
   Index::Editor editor = index_->Edit(docid0, prop0_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
@@ -2548,8 +2595,9 @@
 
   // - Doc 1: Contains 'val0', 'val1', 'val2' in 'prop1'. Should match.
   doc = DocumentBuilder(doc).SetUri("uri1").Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(doc));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(doc)));
   DocumentId docid1 = put_result1.new_document_id;
   editor = index_->Edit(docid1, prop1_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
@@ -2559,8 +2607,9 @@
 
   // - Doc 2: Contains 'val0', 'val1', 'val2' in 'prop2'. Shouldn't match.
   doc = DocumentBuilder(doc).SetUri("uri2").Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(doc));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(doc)));
   DocumentId docid2 = put_result2.new_document_id;
   editor = index_->Edit(docid2, prop2_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
@@ -2570,8 +2619,9 @@
 
   // - Doc 3: Contains 'val0' in 'prop0', 'val1' in 'prop1' etc. Should match.
   doc = DocumentBuilder(doc).SetUri("uri3").Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store_->Put(doc));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store_->Put(document_util::CreateDocumentWrapper(doc)));
   DocumentId docid3 = put_result3.new_document_id;
   editor = index_->Edit(docid3, prop0_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
@@ -2586,8 +2636,9 @@
   // - Doc 4: Contains 'val1' in 'prop0', 'val2' in 'prop1', 'val0' in 'prop2'.
   //          Shouldn't match.
   doc = DocumentBuilder(doc).SetUri("uri4").Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             document_store_->Put(doc));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      document_store_->Put(document_util::CreateDocumentWrapper(doc)));
   DocumentId docid4 = put_result4.new_document_id;
   editor = index_->Edit(docid4, prop0_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("val1", TERM_MATCH_PREFIX));
@@ -2651,8 +2702,9 @@
   // - Doc 0: Contains 'val0', 'val1', 'val2' in 'prop0'. Should match.
   DocumentProto doc =
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(doc));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(document_util::CreateDocumentWrapper(doc)));
   DocumentId docid0 = put_result0.new_document_id;
   Index::Editor editor = index_->Edit(docid0, prop0_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
@@ -2662,8 +2714,9 @@
 
   // - Doc 1: Contains 'val0', 'val1', 'val2' in 'prop1'. Shouldn't match.
   doc = DocumentBuilder(doc).SetUri("uri1").Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(doc));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(doc)));
   DocumentId docid1 = put_result1.new_document_id;
   editor = index_->Edit(docid1, prop1_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
@@ -2673,8 +2726,9 @@
 
   // - Doc 2: Contains 'val0', 'val1', 'val2' in 'prop2'. Should match.
   doc = DocumentBuilder(doc).SetUri("uri2").Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(doc));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(doc)));
   DocumentId docid2 = put_result2.new_document_id;
   editor = index_->Edit(docid2, prop2_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
@@ -2684,8 +2738,9 @@
 
   // - Doc 3: Contains 'val0' in 'prop0', 'val1' in 'prop1' etc. Should match.
   doc = DocumentBuilder(doc).SetUri("uri3").Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store_->Put(doc));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store_->Put(document_util::CreateDocumentWrapper(doc)));
   DocumentId docid3 = put_result3.new_document_id;
   editor = index_->Edit(docid3, prop0_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
@@ -2700,8 +2755,9 @@
   // - Doc 4: Contains 'val1' in 'prop0', 'val2' in 'prop1', 'val0' in 'prop2'.
   //          Shouldn't match.
   doc = DocumentBuilder(doc).SetUri("uri4").Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             document_store_->Put(doc));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      document_store_->Put(document_util::CreateDocumentWrapper(doc)));
   DocumentId docid4 = put_result4.new_document_id;
   editor = index_->Edit(docid4, prop0_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("val1", TERM_MATCH_PREFIX));
@@ -2807,22 +2863,22 @@
   // Section ids are assigned alphabetically.
   SectionId prop1_section_id = 0;
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("type").Build())));
   editor = index_->Edit(kDocumentId2, prop1_section_id,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -2916,8 +2972,9 @@
   NamespaceId ns_id = 0;
   DocumentProto doc =
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(doc));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(document_util::CreateDocumentWrapper(doc)));
   DocumentId docid0 = put_result0.new_document_id;
   Index::Editor editor = index_->Edit(kDocumentId0, prop0_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -2925,7 +2982,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri1").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri1").Build())));
   DocumentId docid1 = put_result1.new_document_id;
   editor = index_->Edit(docid1, prop1_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -2933,7 +2991,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri2").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri2").Build())));
   DocumentId docid2 = put_result2.new_document_id;
   editor = index_->Edit(docid2, prop2_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -2941,7 +3000,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri3").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri3").Build())));
   DocumentId docid3 = put_result3.new_document_id;
   editor = index_->Edit(docid3, prop3_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -2949,7 +3009,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result4,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri4").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri4").Build())));
   DocumentId docid4 = put_result4.new_document_id;
   editor = index_->Edit(docid4, prop4_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -2957,7 +3018,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result5,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri5").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri5").Build())));
   DocumentId docid5 = put_result5.new_document_id;
   editor = index_->Edit(docid5, prop5_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -2965,7 +3027,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result6,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri6").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri6").Build())));
   DocumentId docid6 = put_result6.new_document_id;
   editor = index_->Edit(docid6, prop6_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -2973,7 +3036,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result7,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri7").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri7").Build())));
   DocumentId docid7 = put_result7.new_document_id;
   editor = index_->Edit(docid7, prop7_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3076,8 +3140,9 @@
   NamespaceId ns_id = 0;
   DocumentProto doc =
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(doc));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(document_util::CreateDocumentWrapper(doc)));
   DocumentId docid0 = put_result0.new_document_id;
   Index::Editor editor = index_->Edit(kDocumentId0, prop0_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3085,7 +3150,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri1").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri1").Build())));
   DocumentId docid1 = put_result1.new_document_id;
   editor = index_->Edit(docid1, prop1_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3093,7 +3159,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri2").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri2").Build())));
   DocumentId docid2 = put_result2.new_document_id;
   editor = index_->Edit(docid2, prop2_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3101,7 +3168,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri3").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri3").Build())));
   DocumentId docid3 = put_result3.new_document_id;
   editor = index_->Edit(docid3, prop3_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3109,7 +3177,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result4,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri4").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri4").Build())));
   DocumentId docid4 = put_result4.new_document_id;
   editor = index_->Edit(docid4, prop4_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3117,7 +3186,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result5,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri5").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri5").Build())));
   DocumentId docid5 = put_result5.new_document_id;
   editor = index_->Edit(docid5, prop5_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3125,7 +3195,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result6,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri6").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri6").Build())));
   DocumentId docid6 = put_result6.new_document_id;
   editor = index_->Edit(docid6, prop6_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3133,7 +3204,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result7,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri7").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri7").Build())));
   DocumentId docid7 = put_result7.new_document_id;
   editor = index_->Edit(docid7, prop7_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3208,8 +3280,9 @@
   NamespaceId ns_id = 0;
   DocumentProto doc =
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(doc));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(document_util::CreateDocumentWrapper(doc)));
   DocumentId docid0 = put_result0.new_document_id;
   Index::Editor editor = index_->Edit(kDocumentId0, prop0_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3218,7 +3291,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri1").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri1").Build())));
   DocumentId docid1 = put_result1.new_document_id;
   editor = index_->Edit(docid1, prop0_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
@@ -3226,7 +3300,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri2").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri2").Build())));
   DocumentId docid2 = put_result2.new_document_id;
   editor = index_->Edit(docid2, prop0_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3327,8 +3402,9 @@
   NamespaceId ns_id = 0;
   DocumentProto doc =
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(doc));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(document_util::CreateDocumentWrapper(doc)));
   DocumentId docid0 = put_result0.new_document_id;
   Index::Editor editor = index_->Edit(docid0, prop0_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3337,7 +3413,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri1").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri1").Build())));
   DocumentId docid1 = put_result1.new_document_id;
   editor = index_->Edit(docid1, prop0_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
@@ -3348,7 +3425,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(DocumentBuilder(doc).SetUri("uri2").Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder(doc).SetUri("uri2").Build())));
   DocumentId docid2 = put_result2.new_document_id;
   editor = index_->Edit(docid2, prop0_id, ns_id);
   ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
@@ -3520,17 +3598,21 @@
 
   // Document 0 has the term "foo" and its schema has the url property.
   ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("typeWithUrl").Build()));
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri0")
+                                               .SetSchema("typeWithUrl")
+                                               .Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Document 1 has the term "foo" and its schema DOESN'T have the url property.
-  ICING_ASSERT_OK(document_store_->Put(DocumentBuilder()
-                                           .SetKey("ns", "uri1")
-                                           .SetSchema("typeWithoutUrl")
-                                           .Build()));
+  ICING_ASSERT_OK(document_store_->Put(
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri1")
+                                               .SetSchema("typeWithoutUrl")
+                                               .Build())));
   editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3538,7 +3620,10 @@
 
   // Document 2 has the term "bar" and its schema has the url property.
   ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("typeWithUrl").Build()));
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri2")
+                                               .SetSchema("typeWithUrl")
+                                               .Build())));
   editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
@@ -3569,17 +3654,21 @@
 
   // Document 0 has the term "foo" and its schema has the url property.
   ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("typeWithUrl").Build()));
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri0")
+                                               .SetSchema("typeWithUrl")
+                                               .Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Document 1 has the term "foo" and its schema DOESN'T have the url property.
-  ICING_ASSERT_OK(document_store_->Put(DocumentBuilder()
-                                           .SetKey("ns", "uri1")
-                                           .SetSchema("typeWithoutUrl")
-                                           .Build()));
+  ICING_ASSERT_OK(document_store_->Put(
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri1")
+                                               .SetSchema("typeWithoutUrl")
+                                               .Build())));
   editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3610,17 +3699,21 @@
 
   // Document 0 has the term "foo" and its schema has the url property.
   ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("typeWithUrl").Build()));
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri0")
+                                               .SetSchema("typeWithUrl")
+                                               .Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Document 1 has the term "foo" and its schema DOESN'T have the url property.
-  ICING_ASSERT_OK(document_store_->Put(DocumentBuilder()
-                                           .SetKey("ns", "uri1")
-                                           .SetSchema("typeWithoutUrl")
-                                           .Build()));
+  ICING_ASSERT_OK(document_store_->Put(
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri1")
+                                               .SetSchema("typeWithoutUrl")
+                                               .Build())));
   editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3680,8 +3773,8 @@
       /*ignore_errors_and_delete_documents=*/false));
 
   // Document 0 has the term "foo" and has the "price" property.
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("Simple").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("Simple").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId0,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3691,16 +3784,16 @@
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Document 1 has the term "foo" and doesn't have the "price" property.
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("Simple").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("Simple").Build())));
   editor = index_->Edit(kDocumentId1, kSectionId0,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Document 2 has the term "bar" and has the "price" property.
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri2").SetSchema("Simple").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri2").SetSchema("Simple").Build())));
   editor = index_->Edit(kDocumentId2, kSectionId0,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
@@ -3747,8 +3840,8 @@
       /*ignore_errors_and_delete_documents=*/false));
 
   // Document 0 has the term "foo" and has the "price" property.
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("Simple").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("Simple").Build())));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId0,
                                       /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3758,8 +3851,8 @@
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Document 1 has the term "foo" and doesn't have the "price" property.
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("Simple").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("Simple").Build())));
   editor = index_->Edit(kDocumentId1, kSectionId0,
                         /*namespace_id=*/0);
   ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
@@ -3968,10 +4061,10 @@
                                         .SetCardinality(CARDINALITY_OPTIONAL)))
           .Build(),
       /*ignore_errors_and_delete_documents=*/false));
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
 
   // Index two embedding vectors.
   PropertyProto::VectorProto vector0 =
@@ -3979,9 +4072,11 @@
   PropertyProto::VectorProto vector1 =
       CreateVector("my_model", {-0.1, -0.2, -0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(kSectionId0, kDocumentId0), vector0, QUANTIZATION_TYPE_NONE));
+      BasicHit(kSectionId0, kDocumentId0), vector0, QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(kSectionId0, kDocumentId1), vector1, QUANTIZATION_TYPE_NONE));
+      BasicHit(kSectionId0, kDocumentId1), vector1, QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   // Create an embedding query that has a semantic score of 1 with vector0 and
@@ -4007,22 +4102,19 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId0),
-      Pointee(UnorderedElementsAre(DoubleNear(1, kEps))));
+      UnorderedElementsAre(DoubleNear(1, kEps)));
   if (GetParam().get_embedding_match_info) {
     // Check section match info for document 0.
     const EmbeddingMatchInfos* match_infos =
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId0);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/1,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/1,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
   } else {
-    EXPECT_THAT(
-        query_results.embedding_query_results
-            .GetMatchedInfosForDocument(
-                /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId0)
-            ->section_infos,
-        IsNull());
+    EXPECT_THAT(*query_results.embedding_query_results.global_section_infos,
+                IsEmpty());
   }
 
   // The query should match both vector0 and vector1.
@@ -4039,17 +4131,18 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId0),
-      Pointee(UnorderedElementsAre(DoubleNear(1, kEps))));
+      UnorderedElementsAre(DoubleNear(1, kEps)));
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId1),
-      Pointee(UnorderedElementsAre(DoubleNear(-1, kEps))));
+      UnorderedElementsAre(DoubleNear(-1, kEps)));
   if (GetParam().get_embedding_match_info) {
     // Check section match info for document 0.
     const EmbeddingMatchInfos* match_infos =
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId0);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/1,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/1,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
 
@@ -4057,23 +4150,13 @@
     match_infos =
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId1);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/-1,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/-1,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
   } else {
-    EXPECT_THAT(
-        query_results.embedding_query_results
-            .GetMatchedInfosForDocument(
-                /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId0)
-            ->section_infos,
-        IsNull());
-
-    EXPECT_THAT(
-        query_results.embedding_query_results
-            .GetMatchedInfosForDocument(
-                /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId1)
-            ->section_infos,
-        IsNull());
+    EXPECT_THAT(*query_results.embedding_query_results.global_section_infos,
+                IsEmpty());
   }
 
   // The query should match nothing, since there is no vector with a
@@ -4107,10 +4190,10 @@
                                         .SetCardinality(CARDINALITY_OPTIONAL)))
           .Build(),
       /*ignore_errors_and_delete_documents=*/false));
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
 
   // Index two embedding vectors.
   PropertyProto::VectorProto vector0 =
@@ -4118,9 +4201,11 @@
   PropertyProto::VectorProto vector1 =
       CreateVector("my_model", {-0.1, -0.2, -0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(kSectionId0, kDocumentId0), vector0, QUANTIZATION_TYPE_NONE));
+      BasicHit(kSectionId0, kDocumentId0), vector0, QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(kSectionId0, kDocumentId1), vector1, QUANTIZATION_TYPE_NONE));
+      BasicHit(kSectionId0, kDocumentId1), vector1, QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   // Create an embedding query that has a semantic score of 1 with vector0 and
@@ -4146,22 +4231,19 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId1),
-      Pointee(UnorderedElementsAre(DoubleNear(-1, kEps))));
+      UnorderedElementsAre(DoubleNear(-1, kEps)));
   if (GetParam().get_embedding_match_info) {
     // Check section match info for document 1.
     const EmbeddingMatchInfos* match_infos =
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId1);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/-1,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/-1,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
   } else {
-    EXPECT_THAT(
-        query_results.embedding_query_results
-            .GetMatchedInfosForDocument(
-                /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId1)
-            ->section_infos,
-        IsNull());
+    EXPECT_THAT(*query_results.embedding_query_results.global_section_infos,
+                IsEmpty());
   }
 
   // The query should match both vector0 and vector1.
@@ -4178,17 +4260,18 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId0),
-      Pointee(UnorderedElementsAre(DoubleNear(1, kEps))));
+      UnorderedElementsAre(DoubleNear(1, kEps)));
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId1),
-      Pointee(UnorderedElementsAre(DoubleNear(-1, kEps))));
+      UnorderedElementsAre(DoubleNear(-1, kEps)));
   if (GetParam().get_embedding_match_info) {
     // Check section match info for document 0.
     const EmbeddingMatchInfos* match_infos =
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId0);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/1,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/1,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
 
@@ -4196,23 +4279,14 @@
     match_infos =
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId1);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/-1,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/-1,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
 
   } else {
-    EXPECT_THAT(
-        query_results.embedding_query_results
-            .GetMatchedInfosForDocument(
-                /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId0)
-            ->section_infos,
-        IsNull());
-    EXPECT_THAT(
-        query_results.embedding_query_results
-            .GetMatchedInfosForDocument(
-                /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId1)
-            ->section_infos,
-        IsNull());
+    EXPECT_THAT(*query_results.embedding_query_results.global_section_infos,
+                IsEmpty());
   }
 
   // The query should match nothing, since there is no vector with a
@@ -4246,13 +4320,14 @@
                                         .SetCardinality(CARDINALITY_OPTIONAL)))
           .Build(),
       /*ignore_errors_and_delete_documents=*/false));
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
 
   // Index a embedding vector.
   PropertyProto::VectorProto vector = CreateVector("my_model", {0.1, 0.2, 0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(kSectionId0, kDocumentId0), vector, QUANTIZATION_TYPE_NONE));
+      BasicHit(kSectionId0, kDocumentId0), vector, QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   // Create an embedding query that has:
@@ -4282,22 +4357,19 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId0),
-      Pointee(UnorderedElementsAre(DoubleNear(1, kEps))));
+      UnorderedElementsAre(DoubleNear(1, kEps)));
   if (GetParam().get_embedding_match_info) {
     // Check section match info for document 0.
     const EmbeddingMatchInfos* match_infos =
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId0);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/1,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/1,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
   } else {
-    EXPECT_THAT(
-        query_results.embedding_query_results
-            .GetMatchedInfosForDocument(
-                /*query_vector_index=*/0, EMBEDDING_METRIC_COSINE, kDocumentId0)
-            ->section_infos,
-        IsNull());
+    EXPECT_THAT(*query_results.embedding_query_results.global_section_infos,
+                IsEmpty());
   }
 
   // Create a query that overrides the metric to DOT_PRODUCT.
@@ -4314,24 +4386,21 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId0),
-      Pointee(UnorderedElementsAre(DoubleNear(0.14, kEps))));
+      UnorderedElementsAre(DoubleNear(0.14, kEps)));
   if (GetParam().get_embedding_match_info) {
     // Check section match info for document 0.
     const EmbeddingMatchInfos* match_infos =
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
             kDocumentId0);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/0.14,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/0.14,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
   } else {
     // Check section match info for document 0.
-    EXPECT_THAT(query_results.embedding_query_results
-                    .GetMatchedInfosForDocument(
-                        /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
-                        kDocumentId0)
-                    ->section_infos,
-                IsNull());
+    EXPECT_THAT(*query_results.embedding_query_results.global_section_infos,
+                IsEmpty());
   }
 
   // Create a query that overrides the metric to EUCLIDEAN.
@@ -4349,22 +4418,19 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_EUCLIDEAN, kDocumentId0),
-      Pointee(UnorderedElementsAre(DoubleNear(0, kEps))));
+      UnorderedElementsAre(DoubleNear(0, kEps)));
   if (GetParam().get_embedding_match_info) {
     // Check section match info for document 0.
     const EmbeddingMatchInfos* match_infos =
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_EUCLIDEAN, kDocumentId0);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/0,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/0,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
   } else {
-    EXPECT_THAT(query_results.embedding_query_results
-                    .GetMatchedInfosForDocument(
-                        /*query_vector_index=*/0, EMBEDDING_METRIC_EUCLIDEAN,
-                        kDocumentId0)
-                    ->section_infos,
-                IsNull());
+    EXPECT_THAT(*query_results.embedding_query_results.global_section_infos,
+                IsEmpty());
   }
 }
 
@@ -4386,46 +4452,56 @@
                                         .SetCardinality(CARDINALITY_REPEATED)))
           .Build(),
       /*ignore_errors_and_delete_documents=*/false));
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
 
   // Index vectors for document 0.
   // Section 0
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId0),
-      CreateVector("my_model1", {1, -2, -3}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {1, -2, -3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId0),
-      CreateVector("my_model2", {1, -2, 3, -4}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model2", {1, -2, 3, -4}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId0),
-      CreateVector("my_model1", {-1, -2, 3}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {-1, -2, 3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId0),
-      CreateVector("my_model1", {1, -2, -4}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {1, -2, -4}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   // Section 1
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId1, kDocumentId0),
-      CreateVector("my_model1", {-1, -2, -3}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {-1, -2, -3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId1, kDocumentId0),
-      CreateVector("my_model1", {-1, 2, 4}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {-1, 2, 4}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
 
   // Index embedding vectors for document 1.
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId1),
-      CreateVector("my_model1", {1, -2, 6}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {1, -2, 6}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId1),
-      CreateVector("my_model2", {1, -2, 3, 4}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model2", {1, -2, 3, 4}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId1),
-      CreateVector("my_model1", {-1, -2, -6}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {-1, -2, -6}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId1),
-      CreateVector("my_model2", {-1, -2, -3, -4}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model2", {-1, -2, -3, -4}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
 
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
@@ -4486,11 +4562,11 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId1),
-      Pointee(UnorderedElementsAre(-3, 7)));
+      UnorderedElementsAre(-3, 7));
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId1),
-      Pointee(UnorderedElementsAre(6)));
+      UnorderedElementsAre(6));
 
   // Check results for document 0.
   ICING_ASSERT_OK(itr->Advance());
@@ -4500,11 +4576,11 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId0),
-      Pointee(UnorderedElementsAre(-2, 0, -3, 6, 3)));
+      UnorderedElementsAre(-2, 0, -3, 6, 3));
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId0),
-      Pointee(UnorderedElementsAre(-2)));
+      UnorderedElementsAre(-2));
   EXPECT_THAT(itr->Advance(),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
 
@@ -4523,26 +4599,32 @@
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
             kDocumentId0);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/-2,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/-2,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/-3,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/-3,
                                        /*position_in_section=*/2,
                                        /*section_id=*/kSectionId0));
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/6,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/6,
                                        /*position_in_section=*/1,
                                        /*section_id=*/kSectionId0));
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/0,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/0,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId1));
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/3,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/3,
                                        /*position_in_section=*/1,
                                        /*section_id=*/kSectionId1));
     match_infos =
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT,
             kDocumentId0);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/-2,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/-2,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
 
@@ -4557,44 +4639,25 @@
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
             kDocumentId1);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/-3,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/-3,
                                        /*position_in_section=*/1,
                                        /*section_id=*/kSectionId0));
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/7,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/7,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
     match_infos =
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT,
             kDocumentId1);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/6,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/6,
                                        /*position_in_section=*/1,
                                        /*section_id=*/kSectionId0));
   } else {
-    EXPECT_THAT(query_results.embedding_query_results
-                    .GetMatchedInfosForDocument(
-                        /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
-                        kDocumentId0)
-                    ->section_infos,
-                IsNull());
-    EXPECT_THAT(query_results.embedding_query_results
-                    .GetMatchedInfosForDocument(
-                        /*query_vector_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT,
-                        kDocumentId0)
-                    ->section_infos,
-                IsNull());
-    EXPECT_THAT(query_results.embedding_query_results
-                    .GetMatchedInfosForDocument(
-                        /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
-                        kDocumentId1)
-                    ->section_infos,
-                IsNull());
-    EXPECT_THAT(query_results.embedding_query_results
-                    .GetMatchedInfosForDocument(
-                        /*query_vector_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT,
-                        kDocumentId1)
-                    ->section_infos,
-                IsNull());
+    EXPECT_THAT(*query_results.embedding_query_results.global_section_infos,
+                IsEmpty());
   }
 }
 
@@ -4621,28 +4684,33 @@
                                         .SetCardinality(CARDINALITY_OPTIONAL)))
           .Build(),
       /*ignore_errors_and_delete_documents=*/false));
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
 
   // Index 3 embedding vectors for document 0.
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId0),
-      CreateVector("my_model1", {1, -2, -3}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {1, -2, -3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId1, kDocumentId0),
-      CreateVector("my_model1", {-1, -2, -3}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {-1, -2, -3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId2, kDocumentId0),
-      CreateVector("my_model2", {-1, 2, 3, -4}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model2", {-1, 2, 3, -4}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   // Index 2 embedding vectors for document 1.
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId1),
-      CreateVector("my_model1", {-1, -2, 3}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {-1, -2, 3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId1, kDocumentId1),
-      CreateVector("my_model2", {1, -2, 3, -4}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model2", {1, -2, 3, -4}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   // Create two embedding queries.
@@ -4685,11 +4753,11 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId0),
-      Pointee(UnorderedElementsAre(-2, 0)));
+      UnorderedElementsAre(-2, 0));
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId0),
-      Pointee(UnorderedElementsAre(4)));
+      UnorderedElementsAre(4));
   EXPECT_THAT(itr->Advance(),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
   if (GetParam().get_embedding_match_info) {
@@ -4698,32 +4766,25 @@
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
             kDocumentId0);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/-2,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/-2,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/0,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/0,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId1));
     match_infos =
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT,
             kDocumentId0);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/4,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/4,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId2));
   } else {
-    EXPECT_THAT(query_results.embedding_query_results
-                    .GetMatchedInfosForDocument(
-                        /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
-                        kDocumentId0)
-                    ->section_infos,
-                IsNull());
-    EXPECT_THAT(query_results.embedding_query_results
-                    .GetMatchedInfosForDocument(
-                        /*query_vector_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT,
-                        kDocumentId0)
-                    ->section_infos,
-                IsNull());
+    EXPECT_THAT(*query_results.embedding_query_results.global_section_infos,
+                IsEmpty());
   }
 
   // The query can match both document 0 and document 1:
@@ -4756,11 +4817,11 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId1),
-      Pointee(UnorderedElementsAre(6)));
+      UnorderedElementsAre(6));
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId1),
-      IsNull());
+      IsEmpty());
   // Check results for document 0.
   ICING_ASSERT_OK(itr->Advance());
   EXPECT_THAT(
@@ -4769,11 +4830,11 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId0),
-      IsNull());
+      IsEmpty());
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId0),
-      Pointee(UnorderedElementsAre(4)));
+      UnorderedElementsAre(4));
   EXPECT_THAT(itr->Advance(),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
   if (GetParam().get_embedding_match_info) {
@@ -4787,7 +4848,8 @@
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT,
             kDocumentId0);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/4,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/4,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId2));
 
@@ -4796,7 +4858,8 @@
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
             kDocumentId1);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/6,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/6,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
     match_infos =
@@ -4805,18 +4868,8 @@
             kDocumentId1);
     EXPECT_THAT(match_infos, IsNull());
   } else {
-    EXPECT_THAT(query_results.embedding_query_results
-                    .GetMatchedInfosForDocument(
-                        /*query_vector_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT,
-                        kDocumentId0)
-                    ->section_infos,
-                IsNull());
-    EXPECT_THAT(query_results.embedding_query_results
-                    .GetMatchedInfosForDocument(
-                        /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
-                        kDocumentId1)
-                    ->section_infos,
-                IsNull());
+    EXPECT_THAT(*query_results.embedding_query_results.global_section_infos,
+                IsEmpty());
   }
 }
 
@@ -4839,22 +4892,25 @@
                                         .SetCardinality(CARDINALITY_OPTIONAL)))
           .Build(),
       /*ignore_errors_and_delete_documents=*/false));
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
 
   // Index 3 embedding vectors for document 0.
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId0),
-      CreateVector("my_model1", {1, -2, -3}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {1, -2, -3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId1, kDocumentId0),
-      CreateVector("my_model1", {-1, -2, -3}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {-1, -2, -3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   // Index 2 embedding vectors for document 1.
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId1),
-      CreateVector("my_model1", {-1, -2, 3}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {-1, -2, 3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   // Create two embedding queries.
@@ -4889,7 +4945,7 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId1),
-      Pointee(UnorderedElementsAre(6)));
+      UnorderedElementsAre(6));
   // Check results for document 0.
   ICING_ASSERT_OK(itr->Advance());
   EXPECT_THAT(itr->doc_hit_info(),
@@ -4898,7 +4954,7 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId0),
-      Pointee(UnorderedElementsAre(-2, 0)));
+      UnorderedElementsAre(-2, 0));
   EXPECT_THAT(itr->Advance(),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
   if (GetParam().get_embedding_match_info) {
@@ -4907,10 +4963,12 @@
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
             kDocumentId0);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/-2,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/-2,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/0,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/0,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId1));
     match_infos =
@@ -4924,7 +4982,8 @@
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
             kDocumentId1);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/6,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/6,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
     match_infos =
@@ -4933,18 +4992,8 @@
             kDocumentId0);
     EXPECT_THAT(match_infos, IsNull());
   } else {
-    EXPECT_THAT(query_results.embedding_query_results
-                    .GetMatchedInfosForDocument(
-                        /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
-                        kDocumentId0)
-                    ->section_infos,
-                IsNull());
-    EXPECT_THAT(query_results.embedding_query_results
-                    .GetMatchedInfosForDocument(
-                        /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
-                        kDocumentId1)
-                    ->section_infos,
-                IsNull());
+    EXPECT_THAT(*query_results.embedding_query_results.global_section_infos,
+                IsEmpty());
   }
 
   // The same query appears twice, in which case all the scores in the results
@@ -4968,7 +5017,7 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId1),
-      Pointee(UnorderedElementsAre(6, 6)));
+      UnorderedElementsAre(6, 6));
   // Check results for document 0.
   ICING_ASSERT_OK(itr->Advance());
   EXPECT_THAT(itr->doc_hit_info(),
@@ -4977,7 +5026,7 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId0),
-      Pointee(UnorderedElementsAre(-2, 0, -2, 0)));
+      UnorderedElementsAre(-2, 0, -2, 0));
   EXPECT_THAT(itr->Advance(),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
 }
@@ -5000,10 +5049,10 @@
                                         .SetCardinality(CARDINALITY_OPTIONAL)))
           .Build(),
       /*ignore_errors_and_delete_documents=*/false));
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
 
   // Index terms
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
@@ -5018,10 +5067,12 @@
   // Index embedding vectors
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId0),
-      CreateVector("my_model1", {1, -2, -3}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {1, -2, -3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId1),
-      CreateVector("my_model1", {-1, -2, 3}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {-1, -2, 3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   // Create an embedding query with semantic scores:
@@ -5057,7 +5108,7 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId1),
-      Pointee(UnorderedElementsAre(6)));
+      UnorderedElementsAre(6));
   // Check results for document 0.
   ICING_ASSERT_OK(itr->Advance());
   EXPECT_THAT(
@@ -5066,7 +5117,7 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId0),
-      IsNull());
+      IsEmpty());
   EXPECT_THAT(itr->Advance(),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
   if (GetParam().get_embedding_match_info) {
@@ -5082,16 +5133,13 @@
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
             kDocumentId1);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/6,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/6,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
   } else {
-    EXPECT_THAT(query_results.embedding_query_results
-                    .GetMatchedInfosForDocument(
-                        /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
-                        kDocumentId1)
-                    ->section_infos,
-                IsNull());
+    EXPECT_THAT(*query_results.embedding_query_results.global_section_infos,
+                IsEmpty());
   }
 
   // Perform another hybrid search:
@@ -5118,7 +5166,7 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId0),
-      Pointee(UnorderedElementsAre(-2)));
+      UnorderedElementsAre(-2));
   EXPECT_THAT(itr->Advance(),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
 
@@ -5128,31 +5176,13 @@
         query_results.embedding_query_results.GetMatchedInfosForDocument(
             /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
             kDocumentId0);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/-2,
-                                       /*position_in_section=*/0,
-                                       /*section_id=*/kSectionId0));
-
-    // Check section match info for document 1.
-    match_infos =
-        query_results.embedding_query_results.GetMatchedInfosForDocument(
-            /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
-            kDocumentId1);
-    EXPECT_TRUE(ContainsMatchInfoEntry(match_infos, /*score=*/6,
+    EXPECT_TRUE(ContainsMatchInfoEntry(query_results.embedding_query_results,
+                                       match_infos, /*score=*/-2,
                                        /*position_in_section=*/0,
                                        /*section_id=*/kSectionId0));
   } else {
-    EXPECT_THAT(query_results.embedding_query_results
-                    .GetMatchedInfosForDocument(
-                        /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
-                        kDocumentId0)
-                    ->section_infos,
-                IsNull());
-    EXPECT_THAT(query_results.embedding_query_results
-                    .GetMatchedInfosForDocument(
-                        /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT,
-                        kDocumentId1)
-                    ->section_infos,
-                IsNull());
+    EXPECT_THAT(*query_results.embedding_query_results.global_section_infos,
+                IsEmpty());
   }
 }
 
@@ -5175,23 +5205,27 @@
       /*ignore_errors_and_delete_documents=*/false));
 
   // Create two documents.
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
-  ICING_ASSERT_OK(document_store_->Put(
-      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
   // Add embedding vectors into different sections for the two documents.
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId0),
-      CreateVector("my_model1", {1, -2, -3}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {1, -2, -3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId1, kDocumentId0),
-      CreateVector("my_model1", {-1, -2, 3}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {-1, -2, 3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId1),
-      CreateVector("my_model1", {-1, 2, 3}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {-1, 2, 3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId1, kDocumentId1),
-      CreateVector("my_model1", {1, 2, -3}), QUANTIZATION_TYPE_NONE));
+      CreateVector("my_model1", {1, 2, -3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   // Create an embedding query with semantic scores:
@@ -5223,7 +5257,7 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId1),
-      Pointee(UnorderedElementsAre(2)));
+      UnorderedElementsAre(2));
   ICING_ASSERT_OK(itr->Advance());
   EXPECT_THAT(
       itr->doc_hit_info(),
@@ -5231,11 +5265,171 @@
   EXPECT_THAT(
       query_results.embedding_query_results.GetMatchedScoresForDocument(
           /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId0),
-      Pointee(UnorderedElementsAre(-2)));
+      UnorderedElementsAre(-2));
   EXPECT_THAT(itr->Advance(),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
 }
 
+TEST_P(QueryVisitorTest, SemanticSearchFunctionDeletedDocument) {
+  // Set up schema
+  ICING_ASSERT_OK(schema_store_->SetSchema(
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("type").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("prop1")
+                  .SetDataTypeVector(EMBEDDING_INDEXING_LINEAR_SEARCH)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build(),
+      /*ignore_errors_and_delete_documents=*/false));
+
+  // Add two documents
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
+
+  // Index embedding vectors for both documents
+  PropertyProto::VectorProto vector0 =
+      CreateVector("my_model", {0.1, 0.2, 0.3});
+  PropertyProto::VectorProto vector1 =
+      CreateVector("my_model", {0.4, 0.5, 0.6});
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(kSectionId0, kDocumentId0), vector0, QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(kSectionId0, kDocumentId1), vector1, QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
+  ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
+
+  // Delete kDocumentId0
+  ICING_ASSERT_OK(document_store_->Delete("ns", "uri0",
+                                          clock_.GetSystemTimeMilliseconds()));
+
+  // Create an embedding query that would match both documents if kDocumentId0
+  // was not deleted.
+  std::vector<PropertyProto::VectorProto> embedding_query_vectors = {
+      CreateVector("my_model", {1.0, 1.0, 1.0})};
+  std::string query = "semanticSearch(getEmbeddingParameter(0), -1)";
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<Node> root_node,
+                             ParseQueryHelper(query));
+  SearchSpecProto search_spec =
+      CreateSearchSpec(query, TERM_MATCH_PREFIX, embedding_query_vectors,
+                       EMBEDDING_METRIC_DOT_PRODUCT);
+  ICING_ASSERT_OK_AND_ASSIGN(QueryResults query_results,
+                             ProcessQuery(search_spec, root_node.get()));
+
+  EXPECT_THAT(ExtractKeys(query_results.query_term_iterators), IsEmpty());
+  EXPECT_THAT(query_results.query_terms, IsEmpty());
+  EXPECT_THAT(query_results.features_in_use,
+              UnorderedElementsAre(kListFilterQueryLanguageFeature));
+
+  // Only kDocumentId1 should be returned
+  EXPECT_THAT(GetDocumentIds(query_results.root_iterator.get()),
+              ElementsAre(kDocumentId1));
+
+  // Check that scores for kDocumentId1 are present and kDocumentId0 are not.
+  EXPECT_THAT(
+      query_results.embedding_query_results.GetMatchedScoresForDocument(
+          /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId1),
+      UnorderedElementsAre(DoubleNear(0.4 + 0.5 + 0.6, kEps)));
+  EXPECT_THAT(
+      query_results.embedding_query_results.GetMatchedScoresForDocument(
+          /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId0),
+      IsEmpty());
+}
+
+TEST_P(QueryVisitorTest, SemanticSearchFunctionCallStats) {
+  if (!GetParam().enable_embedding_iterator_v2) {
+    // Call stats are only populated in embedding iterator v2.
+    return;
+  }
+
+  // Set up
+  ICING_ASSERT_OK(schema_store_->SetSchema(
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("type")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("prop1")
+                                        .SetDataTypeVector(
+                                            EMBEDDING_INDEXING_LINEAR_SEARCH)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("prop2")
+                                        .SetDataTypeVector(
+                                            EMBEDDING_INDEXING_LINEAR_SEARCH,
+                                            QUANTIZATION_TYPE_QUANTIZE_8_BIT)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build(),
+      /*ignore_errors_and_delete_documents=*/false));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build())));
+  ICING_ASSERT_OK(document_store_->Put(document_util::CreateDocumentWrapper(
+      DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build())));
+
+  // Index 2 unquantized embedding vectors, and 1 quantized embedding vector.
+  PropertyProto::VectorProto vector0 =
+      CreateVector("my_model", {1., 1., 1.});  // Size: 12 bytes
+  PropertyProto::VectorProto vector1 =
+      CreateVector("my_model", {-1., -1., -1.});  // Size: 12 bytes
+  PropertyProto::VectorProto vector2 = CreateVector(
+      "my_model", {0., 0., 0.});  // Size: 3 + sizeof(Quantizer) = 11 bytes.
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(kSectionId0, kDocumentId0), vector0, QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(kSectionId0, kDocumentId1), vector1, QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"type"));
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(kSectionId1, kDocumentId1), vector2,
+      QUANTIZATION_TYPE_QUANTIZE_8_BIT,
+      /*schema_name=*/"type"));
+  ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
+
+  // Create a query that matches all embeddings.
+  std::vector<PropertyProto::VectorProto> embedding_query_vectors = {
+      CreateVector("my_model", {1., 1., 1.})};
+  std::string query = "semanticSearch(getEmbeddingParameter(0))";
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<Node> root_node,
+                             ParseQueryHelper(query));
+  SearchSpecProto search_spec =
+      CreateSearchSpec(query, TERM_MATCH_PREFIX, embedding_query_vectors,
+                       EMBEDDING_METRIC_DOT_PRODUCT);
+  ICING_ASSERT_OK_AND_ASSIGN(QueryResults query_results,
+                             ProcessQuery(search_spec, root_node.get()));
+  EXPECT_THAT(ExtractKeys(query_results.query_term_iterators), IsEmpty());
+  EXPECT_THAT(query_results.query_terms, IsEmpty());
+  EXPECT_THAT(query_results.features_in_use,
+              UnorderedElementsAre(kListFilterQueryLanguageFeature));
+  EXPECT_THAT(GetDocumentIds(query_results.root_iterator.get()),
+              ElementsAre(kDocumentId1, kDocumentId0));
+  EXPECT_THAT(
+      query_results.embedding_query_results.GetMatchedScoresForDocument(
+          /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId0),
+      UnorderedElementsAre(DoubleNear(3., kEps)));
+  EXPECT_THAT(
+      query_results.embedding_query_results.GetMatchedScoresForDocument(
+          /*query_vector_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT, kDocumentId1),
+      UnorderedElementsAre(DoubleNear(-3., kEps), DoubleNear(0, kEps)));
+
+  // Check call stats.
+  // We should see 2 unquantized embeddings scored and 1 quantized embedding
+  // scored. The size read should be 12 + 12 + 11 = 35 bytes.
+  EXPECT_THAT(query_results.root_iterator->GetCallStats(),
+              EqualsDocHitInfoIteratorCallStats(
+                  /*num_leaf_advance_calls_lite_index=*/2,
+                  /*num_leaf_advance_calls_main_index=*/0,
+                  /*num_leaf_advance_calls_integer_index=*/0,
+                  /*num_leaf_advance_calls_no_index=*/0,
+                  /*num_blocks_inspected=*/0,
+                  DocHitInfoIterator::CallStats::EmbeddingStats{
+                      .num_unquantized_embeddings_scored = 2,
+                      .num_quantized_embeddings_scored = 1,
+                      .unquantized_shards_read = {5},
+                      .quantized_shards_read = {5},
+                      .num_embedding_bytes_read = 35}));
+}
+
 TEST_P(QueryVisitorTest,
        MatchScoreExpressionFunctionWithInvalidRangeReturnsInvalidArgument) {
   // The expression is invalid, since low > high.
@@ -5258,16 +5452,18 @@
           .AddType(SchemaTypeConfigBuilder().SetType("Simple"))
           .Build(),
       /*ignore_errors_and_delete_documents=*/false));
-  ICING_ASSERT_OK(document_store_->Put(DocumentBuilder()
-                                           .SetKey("ns", "uri0")
-                                           .SetSchema("Simple")
-                                           .SetScore(4)
-                                           .Build()));
-  ICING_ASSERT_OK(document_store_->Put(DocumentBuilder()
-                                           .SetKey("ns", "uri1")
-                                           .SetSchema("Simple")
-                                           .SetScore(0)
-                                           .Build()));
+  ICING_ASSERT_OK(document_store_->Put(
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri0")
+                                               .SetSchema("Simple")
+                                               .SetScore(4)
+                                               .Build())));
+  ICING_ASSERT_OK(document_store_->Put(
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri1")
+                                               .SetSchema("Simple")
+                                               .SetScore(0)
+                                               .Build())));
 
   // Test that `matchScoreExpression("this.documentScore()", 0)` matches
   // all documents.
@@ -5309,16 +5505,18 @@
           .AddType(SchemaTypeConfigBuilder().SetType("Simple"))
           .Build(),
       /*ignore_errors_and_delete_documents=*/false));
-  ICING_ASSERT_OK(document_store_->Put(DocumentBuilder()
-                                           .SetKey("ns", "uri0")
-                                           .SetSchema("Simple")
-                                           .SetScore(4)
-                                           .Build()));
-  ICING_ASSERT_OK(document_store_->Put(DocumentBuilder()
-                                           .SetKey("ns", "uri1")
-                                           .SetSchema("Simple")
-                                           .SetScore(0)
-                                           .Build()));
+  ICING_ASSERT_OK(document_store_->Put(
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri0")
+                                               .SetSchema("Simple")
+                                               .SetScore(4)
+                                               .Build())));
+  ICING_ASSERT_OK(document_store_->Put(
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri1")
+                                               .SetSchema("Simple")
+                                               .SetScore(0)
+                                               .Build())));
 
   // Test that `matchScoreExpression("this.documentScore()", -100, 100)` matches
   // all documents.
@@ -5360,24 +5558,27 @@
           .AddType(SchemaTypeConfigBuilder().SetType("Simple"))
           .Build(),
       /*ignore_errors_and_delete_documents=*/false));
-  ICING_ASSERT_OK(document_store_->Put(DocumentBuilder()
-                                           .SetKey("ns", "uri0")
-                                           .SetSchema("Simple")
-                                           .SetScore(4)
-                                           .SetCreationTimestampMs(1)
-                                           .Build()));
-  ICING_ASSERT_OK(document_store_->Put(DocumentBuilder()
-                                           .SetKey("ns", "uri1")
-                                           .SetSchema("Simple")
-                                           .SetScore(2)
-                                           .SetCreationTimestampMs(2)
-                                           .Build()));
-  ICING_ASSERT_OK(document_store_->Put(DocumentBuilder()
-                                           .SetKey("ns", "uri2")
-                                           .SetSchema("Simple")
-                                           .SetScore(0)
-                                           .SetCreationTimestampMs(3)
-                                           .Build()));
+  ICING_ASSERT_OK(document_store_->Put(
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri0")
+                                               .SetSchema("Simple")
+                                               .SetScore(4)
+                                               .SetCreationTimestampMs(1)
+                                               .Build())));
+  ICING_ASSERT_OK(document_store_->Put(
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri1")
+                                               .SetSchema("Simple")
+                                               .SetScore(2)
+                                               .SetCreationTimestampMs(2)
+                                               .Build())));
+  ICING_ASSERT_OK(document_store_->Put(
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri2")
+                                               .SetSchema("Simple")
+                                               .SetScore(0)
+                                               .SetCreationTimestampMs(3)
+                                               .Build())));
 
   // Query with a complex score expression:
   //   `pow(this.creationTimestamp(), 2) + this.documentScore() - 1`.
@@ -5404,16 +5605,18 @@
           .AddType(SchemaTypeConfigBuilder().SetType("Simple"))
           .Build(),
       /*ignore_errors_and_delete_documents=*/false));
-  ICING_ASSERT_OK(document_store_->Put(DocumentBuilder()
-                                           .SetKey("ns", "uri0")
-                                           .SetSchema("Simple")
-                                           .SetScore(4)
-                                           .Build()));
-  ICING_ASSERT_OK(document_store_->Put(DocumentBuilder()
-                                           .SetKey("ns", "uri1")
-                                           .SetSchema("Simple")
-                                           .SetScore(0)
-                                           .Build()));
+  ICING_ASSERT_OK(document_store_->Put(
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri0")
+                                               .SetSchema("Simple")
+                                               .SetScore(4)
+                                               .Build())));
+  ICING_ASSERT_OK(document_store_->Put(
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("ns", "uri1")
+                                               .SetSchema("Simple")
+                                               .SetScore(0)
+                                               .Build())));
 
   // Test that documents with evaluation errors will be filtered out.
   // Specifically, document1 will be filtered out because its document score is
@@ -5431,10 +5634,71 @@
 
 INSTANTIATE_TEST_SUITE_P(
     QueryVisitorTest, QueryVisitorTest,
-    testing::Values(QueryVisitorTestParams(QueryType::kSearch, true),
-                    QueryVisitorTestParams(QueryType::kSearch, false),
-                    QueryVisitorTestParams(QueryType::kPlain, true),
-                    QueryVisitorTestParams(QueryType::kPlain, false)));
+    testing::Values(
+        QueryVisitorTestParams(QueryType::kSearch,
+                               /*get_embedding_match_info=*/true,
+                               /*enable_embedding_iterator_v2=*/true,
+                               /*enable_embed_query_optimization=*/false),
+        QueryVisitorTestParams(QueryType::kSearch,
+                               /*get_embedding_match_info=*/true,
+                               /*enable_embedding_iterator_v2=*/false,
+                               /*enable_embed_query_optimization=*/false),
+        QueryVisitorTestParams(QueryType::kSearch,
+                               /*get_embedding_match_info=*/false,
+                               /*enable_embedding_iterator_v2=*/true,
+                               /*enable_embed_query_optimization=*/false),
+        QueryVisitorTestParams(QueryType::kSearch,
+                               /*get_embedding_match_info=*/false,
+                               /*enable_embedding_iterator_v2=*/false,
+                               /*enable_embed_query_optimization=*/false),
+        QueryVisitorTestParams(QueryType::kPlain,
+                               /*get_embedding_match_info=*/true,
+                               /*enable_embedding_iterator_v2=*/true,
+                               /*enable_embed_query_optimization=*/false),
+        QueryVisitorTestParams(QueryType::kPlain,
+                               /*get_embedding_match_info=*/true,
+                               /*enable_embedding_iterator_v2=*/false,
+                               /*enable_embed_query_optimization=*/false),
+        QueryVisitorTestParams(QueryType::kPlain,
+                               /*get_embedding_match_info=*/false,
+                               /*enable_embedding_iterator_v2=*/true,
+                               /*enable_embed_query_optimization=*/false),
+        QueryVisitorTestParams(QueryType::kPlain,
+                               /*get_embedding_match_info=*/false,
+                               /*enable_embedding_iterator_v2=*/false,
+                               /*enable_embed_query_optimization=*/false),
+        QueryVisitorTestParams(QueryType::kSearch,
+                               /*get_embedding_match_info=*/true,
+                               /*enable_embedding_iterator_v2=*/true,
+                               /*enable_embed_query_optimization=*/true),
+        QueryVisitorTestParams(QueryType::kSearch,
+                               /*get_embedding_match_info=*/true,
+                               /*enable_embedding_iterator_v2=*/false,
+                               /*enable_embed_query_optimization=*/true),
+        QueryVisitorTestParams(QueryType::kSearch,
+                               /*get_embedding_match_info=*/false,
+                               /*enable_embedding_iterator_v2=*/true,
+                               /*enable_embed_query_optimization=*/true),
+        QueryVisitorTestParams(QueryType::kSearch,
+                               /*get_embedding_match_info=*/false,
+                               /*enable_embedding_iterator_v2=*/false,
+                               /*enable_embed_query_optimization=*/true),
+        QueryVisitorTestParams(QueryType::kPlain,
+                               /*get_embedding_match_info=*/true,
+                               /*enable_embedding_iterator_v2=*/true,
+                               /*enable_embed_query_optimization=*/true),
+        QueryVisitorTestParams(QueryType::kPlain,
+                               /*get_embedding_match_info=*/true,
+                               /*enable_embedding_iterator_v2=*/false,
+                               /*enable_embed_query_optimization=*/true),
+        QueryVisitorTestParams(QueryType::kPlain,
+                               /*get_embedding_match_info=*/false,
+                               /*enable_embedding_iterator_v2=*/true,
+                               /*enable_embed_query_optimization=*/true),
+        QueryVisitorTestParams(QueryType::kPlain,
+                               /*get_embedding_match_info=*/false,
+                               /*enable_embedding_iterator_v2=*/false,
+                               /*enable_embed_query_optimization=*/true)));
 
 }  // namespace
 
diff --git a/icing/query/query-processor.cc b/icing/query/query-processor.cc
index 7830dc5..af200b9 100644
--- a/icing/query/query-processor.cc
+++ b/icing/query/query-processor.cc
@@ -29,9 +29,11 @@
 #include "icing/index/iterator/doc-hit-info-iterator-all-document-id.h"
 #include "icing/index/iterator/doc-hit-info-iterator-and.h"
 #include "icing/index/iterator/doc-hit-info-iterator-by-uri.h"
+#include "icing/index/iterator/doc-hit-info-iterator-data-holder.h"
 #include "icing/index/iterator/doc-hit-info-iterator-filter.h"
 #include "icing/index/iterator/doc-hit-info-iterator-section-restrict.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/index/iterator/document-filter-predicate.h"
 #include "icing/index/numeric/numeric-index.h"
 #include "icing/join/join-children-fetcher.h"
 #include "icing/proto/logging.pb.h"
@@ -103,10 +105,15 @@
     ScoringSpecProto::RankingStrategy::Code ranking_strategy,
     bool get_embedding_match_info, int64_t current_time_ms,
     QueryStatsProto::SearchStats* search_stats) {
-  ICING_ASSIGN_OR_RETURN(QueryResults results,
-                         ParseAdvancedQuery(search_spec, ranking_strategy,
-                                            get_embedding_match_info,
-                                            current_time_ms, search_stats));
+  std::unique_ptr<DocumentFilterPredicate> filter_predicate =
+      GetFilterPredicateBySchemaAndNamespace(search_spec, document_store_,
+                                             schema_store_, current_time_ms);
+
+  ICING_ASSIGN_OR_RETURN(
+      QueryResults results,
+      ParseAdvancedQuery(search_spec, ranking_strategy,
+                         get_embedding_match_info, current_time_ms,
+                         filter_predicate.get(), search_stats));
 
   // Check that all new features used in the search have been enabled in the
   // SearchSpec.
@@ -138,11 +145,12 @@
   }
   results.root_iterator = CreateAndIterator(std::move(iterators));
 
-  DocHitInfoIteratorFilter::Options options =
-      GetFilterOptions(search_spec, document_store_, schema_store_);
-  results.root_iterator = std::make_unique<DocHitInfoIteratorFilter>(
-      std::move(results.root_iterator), &document_store_, &schema_store_,
-      options, current_time_ms);
+  results.root_iterator = DocHitInfoIteratorFilter::ApplyFilter(
+      std::move(results.root_iterator), filter_predicate.get(),
+      feature_flags_.enable_passing_filter_to_children());
+  results.root_iterator =
+      std::make_unique<DocHitInfoIteratorDataHolder<DocumentFilterPredicate>>(
+          std::move(results.root_iterator), std::move(filter_predicate));
   if (!search_spec.type_property_filters().empty()) {
     results.root_iterator =
         DocHitInfoIteratorSectionRestrict::ApplyRestrictions(
@@ -156,6 +164,7 @@
     const SearchSpecProto& search_spec,
     ScoringSpecProto::RankingStrategy::Code ranking_strategy,
     bool get_embedding_match_info, int64_t current_time_ms,
+    const DocumentFilterPredicate* filter_predicate,
     QueryStatsProto::SearchStats* search_stats) const {
   std::unique_ptr<Timer> lexer_timer = clock_.GetNewTimer();
   Lexer lexer(search_spec.query(), Lexer::Language::QUERY);
@@ -182,8 +191,6 @@
       std::unique_ptr<Tokenizer> plain_tokenizer,
       tokenizer_factory::CreateIndexingTokenizer(
           StringIndexingConfig::TokenizerType::PLAIN, &language_segmenter_));
-  DocHitInfoIteratorFilter::Options options =
-      GetFilterOptions(search_spec, document_store_, schema_store_);
   bool needs_term_frequency_info =
       ranking_strategy == ScoringSpecProto::RankingStrategy::RELEVANCE_SCORE;
 
@@ -191,7 +198,7 @@
   QueryVisitor query_visitor(
       &index_, &numeric_index_, &embedding_index_, &document_store_,
       &schema_store_, &normalizer_, plain_tokenizer.get(),
-      join_children_fetcher_, search_spec, std::move(options),
+      join_children_fetcher_, search_spec, filter_predicate,
       needs_term_frequency_info, get_embedding_match_info, &feature_flags_,
       current_time_ms);
   tree_root->Accept(&query_visitor);
diff --git a/icing/query/query-processor.h b/icing/query/query-processor.h
index 03eb10b..69bd75f 100644
--- a/icing/query/query-processor.h
+++ b/icing/query/query-processor.h
@@ -22,6 +22,7 @@
 #include "icing/feature-flags.h"
 #include "icing/index/embed/embedding-index.h"
 #include "icing/index/index.h"
+#include "icing/index/iterator/document-filter-predicate.h"
 #include "icing/index/numeric/numeric-index.h"
 #include "icing/join/join-children-fetcher.h"
 #include "icing/proto/logging.pb.h"
@@ -98,6 +99,7 @@
       const SearchSpecProto& search_spec,
       ScoringSpecProto::RankingStrategy::Code ranking_strategy,
       bool get_embedding_match_info, int64_t current_time_ms,
+      const DocumentFilterPredicate* filter_predicate,
       QueryStatsProto::SearchStats* search_stats) const;
 
   // Parse the query into a one DocHitInfoIterator that represents the root of a
diff --git a/icing/query/query-processor_benchmark.cc b/icing/query/query-processor_benchmark.cc
index ad125c9..1cc3d19 100644
--- a/icing/query/query-processor_benchmark.cc
+++ b/icing/query/query-processor_benchmark.cc
@@ -30,6 +30,7 @@
 #include "icing/index/index.h"
 #include "icing/index/numeric/dummy-numeric-index.h"
 #include "icing/legacy/index/icing-filesystem.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/proto/search.pb.h"
 #include "icing/proto/term.pb.h"
@@ -49,6 +50,7 @@
 #include "icing/transform/normalizer-options.h"
 #include "icing/transform/normalizer.h"
 #include "icing/util/clock.h"
+#include "icing/util/document-util.h"
 #include "icing/util/icu-data-file-helper.h"
 #include "icing/util/logging.h"
 #include "unicode/uloc.h"
@@ -97,11 +99,13 @@
 
 std::unique_ptr<Index> CreateIndex(const IcingFilesystem& icing_filesystem,
                                    const Filesystem& filesystem,
-                                   const std::string& index_dir) {
+                                   const std::string& index_dir,
+                                   const FeatureFlags& feature_flags) {
   Index::Options options(index_dir, /*index_merge_size=*/1024 * 1024 * 10,
                          /*lite_index_sort_at_indexing=*/true,
                          /*lite_index_sort_size=*/1024 * 8);
-  return Index::Create(options, &filesystem, &icing_filesystem).ValueOrDie();
+  return Index::Create(options, &filesystem, &icing_filesystem, &feature_flags)
+      .ValueOrDie();
 }
 
 std::unique_ptr<Normalizer> CreateNormalizer() {
@@ -119,6 +123,9 @@
       /*force_recovery_and_revalidate_documents=*/false,
       /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
       PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
+      PortableFileBackedProtoLog<
+          DocumentWrapper>::kDefaultCompressionThresholdBytes,
+      protobuf_ports::kDefaultMemLevel,
       /*initialize_stats=*/nullptr);
 }
 
@@ -147,7 +154,7 @@
   }
 
   std::unique_ptr<Index> index =
-      CreateIndex(icing_filesystem, filesystem, index_dir);
+      CreateIndex(icing_filesystem, filesystem, index_dir, feature_flags);
   // TODO(b/249829533): switch to use persistent numeric index.
   ICING_ASSERT_OK_AND_ASSIGN(
       auto numeric_index,
@@ -178,13 +185,15 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto embedding_index,
       EmbeddingIndex::Create(&filesystem, embedding_index_dir, &clock,
-                             &feature_flags));
+                             &feature_flags,
+                             /*num_shards=*/32));
 
   DocumentId document_id = document_store
-                               ->Put(DocumentBuilder()
-                                         .SetKey("icing", "type1")
-                                         .SetSchema("type1")
-                                         .Build())
+                               ->Put(document_util::CreateDocumentWrapper(
+                                   DocumentBuilder()
+                                       .SetKey("icing", "type1")
+                                       .SetSchema("type1")
+                                       .Build()))
                                .ValueOrDie()
                                .new_document_id;
 
@@ -286,7 +295,7 @@
   }
 
   std::unique_ptr<Index> index =
-      CreateIndex(icing_filesystem, filesystem, index_dir);
+      CreateIndex(icing_filesystem, filesystem, index_dir, feature_flags);
   // TODO(b/249829533): switch to use persistent numeric index.
   ICING_ASSERT_OK_AND_ASSIGN(
       auto numeric_index,
@@ -317,13 +326,15 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto embedding_index,
       EmbeddingIndex::Create(&filesystem, embedding_index_dir, &clock,
-                             &feature_flags));
+                             &feature_flags,
+                             /*num_shards=*/32));
 
   DocumentId document_id = document_store
-                               ->Put(DocumentBuilder()
-                                         .SetKey("icing", "type1")
-                                         .SetSchema("type1")
-                                         .Build())
+                               ->Put(document_util::CreateDocumentWrapper(
+                                   DocumentBuilder()
+                                       .SetKey("icing", "type1")
+                                       .SetSchema("type1")
+                                       .Build()))
                                .ValueOrDie()
                                .new_document_id;
 
@@ -443,7 +454,7 @@
   }
 
   std::unique_ptr<Index> index =
-      CreateIndex(icing_filesystem, filesystem, index_dir);
+      CreateIndex(icing_filesystem, filesystem, index_dir, feature_flags);
   // TODO(b/249829533): switch to use persistent numeric index.
   ICING_ASSERT_OK_AND_ASSIGN(
       auto numeric_index,
@@ -474,13 +485,15 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto embedding_index,
       EmbeddingIndex::Create(&filesystem, embedding_index_dir, &clock,
-                             &feature_flags));
+                             &feature_flags,
+                             /*num_shards=*/32));
 
   DocumentId document_id = document_store
-                               ->Put(DocumentBuilder()
-                                         .SetKey("icing", "type1")
-                                         .SetSchema("type1")
-                                         .Build())
+                               ->Put(document_util::CreateDocumentWrapper(
+                                   DocumentBuilder()
+                                       .SetKey("icing", "type1")
+                                       .SetSchema("type1")
+                                       .Build()))
                                .ValueOrDie()
                                .new_document_id;
 
@@ -585,7 +598,7 @@
   }
 
   std::unique_ptr<Index> index =
-      CreateIndex(icing_filesystem, filesystem, index_dir);
+      CreateIndex(icing_filesystem, filesystem, index_dir, feature_flags);
   // TODO(b/249829533): switch to use persistent numeric index.
   ICING_ASSERT_OK_AND_ASSIGN(
       auto numeric_index,
@@ -616,13 +629,15 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto embedding_index,
       EmbeddingIndex::Create(&filesystem, embedding_index_dir, &clock,
-                             &feature_flags));
+                             &feature_flags,
+                             /*num_shards=*/32));
 
   DocumentId document_id = document_store
-                               ->Put(DocumentBuilder()
-                                         .SetKey("icing", "type1")
-                                         .SetSchema("type1")
-                                         .Build())
+                               ->Put(document_util::CreateDocumentWrapper(
+                                   DocumentBuilder()
+                                       .SetKey("icing", "type1")
+                                       .SetSchema("type1")
+                                       .Build()))
                                .ValueOrDie()
                                .new_document_id;
 
diff --git a/icing/query/query-processor_test.cc b/icing/query/query-processor_test.cc
index b5d1f0f..37aa49e 100644
--- a/icing/query/query-processor_test.cc
+++ b/icing/query/query-processor_test.cc
@@ -41,6 +41,7 @@
 #include "icing/index/numeric/numeric-index.h"
 #include "icing/jni/jni-cache.h"
 #include "icing/legacy/index/icing-filesystem.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/logging.pb.h"
 #include "icing/proto/schema.pb.h"
@@ -54,6 +55,7 @@
 #include "icing/store/document-id.h"
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
+#include "icing/testing/embedding-test-utils.h"
 #include "icing/testing/fake-clock.h"
 #include "icing/testing/jni-test-helpers.h"
 #include "icing/testing/test-data.h"
@@ -65,6 +67,7 @@
 #include "icing/transform/normalizer-options.h"
 #include "icing/transform/normalizer.h"
 #include "icing/util/clock.h"
+#include "icing/util/document-util.h"
 #include "icing/util/icu-data-file-helper.h"
 #include "icing/util/status-macros.h"
 #include "unicode/uloc.h"
@@ -89,6 +92,9 @@
       /*force_recovery_and_revalidate_documents=*/false,
       /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
       PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
+      PortableFileBackedProtoLog<
+          DocumentWrapper>::kDefaultCompressionThresholdBytes,
+      protobuf_ports::kDefaultMemLevel,
       /*initialize_stats=*/nullptr);
 }
 
@@ -136,7 +142,8 @@
                            /*lite_index_sort_at_indexing=*/true,
                            /*lite_index_sort_size=*/1024 * 8);
     ICING_ASSERT_OK_AND_ASSIGN(
-        index_, Index::Create(options, &filesystem_, &icing_filesystem_));
+        index_, Index::Create(options, &filesystem_, &icing_filesystem_,
+                              feature_flags_.get()));
     // TODO(b/249829533): switch to use persistent numeric index.
     ICING_ASSERT_OK_AND_ASSIGN(
         numeric_index_,
@@ -144,7 +151,8 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         embedding_index_,
         EmbeddingIndex::Create(&filesystem_, embedding_index_dir_, &fake_clock_,
-                               feature_flags_.get()));
+                               feature_flags_.get(),
+                               /*num_shards=*/32));
 
     language_segmenter_factory::SegmenterOptions segmenter_options(
         ULOC_US, jni_cache_.get());
@@ -303,17 +311,21 @@
                   schema, /*ignore_errors_and_delete_documents=*/false),
               IsOk());
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // We don't need to insert anything in the index since the empty query will
@@ -347,11 +359,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id = put_result.new_document_id;
 
   // Populate the index
@@ -408,11 +422,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id = put_result.new_document_id;
 
   // Populate the index
@@ -464,11 +480,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id = put_result.new_document_id;
 
   // Populate the index
@@ -522,11 +540,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id = put_result.new_document_id;
 
   // Populate the index
@@ -578,11 +598,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that the DocHitInfoIterators will see
   // that the document exists and not filter out the DocumentId as deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id = put_result.new_document_id;
 
   // Populate the index
@@ -636,11 +658,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that the DocHitInfoIterators will see
   // that the document exists and not filter out the DocumentId as deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id = put_result.new_document_id;
 
   // Populate the index
@@ -697,11 +721,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that the DocHitInfoIterators will see
   // that the document exists and not filter out the DocumentId as deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id = put_result.new_document_id;
 
   // Populate the index
@@ -755,11 +781,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that the DocHitInfoIterators will see
   // that the document exists and not filter out the DocumentId as deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id = put_result.new_document_id;
 
   // Populate the index
@@ -817,11 +845,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that the DocHitInfoIterators will see
   // that the document exists and not filter out the DocumentId as deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id = put_result.new_document_id;
 
   // Populate the index
@@ -879,17 +909,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that the DocHitInfoIterators will see
   // that the document exists and not filter out the DocumentId as deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Populate the index
@@ -955,17 +989,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that the DocHitInfoIterators will see
   // that the document exists and not filter out the DocumentId as deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Populate the index
@@ -1031,17 +1069,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that the DocHitInfoIterators will see
   // that the document exists and not filter out the DocumentId as deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Populate the index
@@ -1105,17 +1147,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that the DocHitInfoIterators will see
   // that the document exists and not filter out the DocumentId as deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
   // Populate the index
   SectionId section_id = 0;
@@ -1280,17 +1326,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that the DocHitInfoIterators will see
   // that the document exists and not filter out the DocumentId as deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Populate the index
@@ -1348,17 +1398,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that the DocHitInfoIterators will see
   // that the document exists and not filter out the DocumentId as deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Populate the index
@@ -1419,17 +1473,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that the DocHitInfoIterators will see
   // that the document exists and not filter out the DocumentId as deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Populate the index
@@ -1487,17 +1545,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that the DocHitInfoIterators will see
   // that the document exists and not filter out the DocumentId as deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Populate the index
@@ -1557,17 +1619,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that they'll bump the
   // last_added_document_id, which will give us the proper exclusion results
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Populate the index
@@ -1613,17 +1679,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that they'll bump the
   // last_added_document_id, which will give us the proper exclusion results
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
   // Populate the index
   SectionId section_id = 0;
@@ -1667,17 +1737,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that they'll bump the
   // last_added_document_id, which will give us the proper exclusion results
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Populate the index
@@ -1754,17 +1828,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that they'll bump the
   // last_added_document_id, which will give us the proper exclusion results
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Populate the index
@@ -1844,17 +1922,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // just inserting the documents so that the DocHitInfoIterators will see
   // that the document exists and not filter out the DocumentId as deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Populate the index
@@ -1945,17 +2027,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
   EXPECT_THAT(document_store_->Delete("namespace", "1",
                                       fake_clock_.GetSystemTimeMilliseconds()),
@@ -2013,17 +2099,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace2", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace2", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Populate the index
@@ -2081,17 +2171,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // schema types populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("message")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("message")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Populate the index
@@ -2150,11 +2244,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // schema types populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id = put_result.new_document_id;
 
   // Populate the index
@@ -2223,17 +2319,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // schema types populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId email_document_id = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("message")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("message")
+                                                   .Build())));
   DocumentId message_document_id = put_result2.new_document_id;
 
   // Populate the index
@@ -2298,17 +2398,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // schema types populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId email_document_id = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("message")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("message")
+                                                   .Build())));
   DocumentId message_document_id = put_result2.new_document_id;
 
   // Populate the index
@@ -2391,11 +2495,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // schema types populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId email_document_id = put_result1.new_document_id;
 
   // Populate the index
@@ -2456,17 +2562,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // schema types populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId email_document_id = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("message")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("message")
+                                                   .Build())));
   DocumentId message_document_id = put_result2.new_document_id;
 
   // Populate the index
@@ -2521,11 +2631,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // schema types populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId email_document_id = put_result1.new_document_id;
 
   // Populate the index
@@ -2579,11 +2691,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // schema types populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId email_document_id = put_result1.new_document_id;
 
   // Populate the index
@@ -2640,17 +2754,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // schema types populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId email_document_id = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("message")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("message")
+                                                   .Build())));
   DocumentId message_document_id = put_result2.new_document_id;
 
   // Poplate the index
@@ -2750,17 +2868,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // schema types populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId email_document_id = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("message")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("message")
+                                                   .Build())));
   DocumentId message_document_id = put_result2.new_document_id;
 
   // Poplate the index
@@ -2881,17 +3003,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // schema types populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId email_document_id = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("message")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("message")
+                                                   .Build())));
   DocumentId message_document_id = put_result2.new_document_id;
 
   // Poplate the index
@@ -2957,6 +3083,308 @@
   EXPECT_THAT(results.query_terms["foo"], UnorderedElementsAre("animal"));
 }
 
+TEST_F(QueryProcessorTest, TypePropertyFilterHybridSearchRestrictTerm) {
+  // Create the schema and document store
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("foo")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("bar")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("propEmbed0")
+                                        .SetDataTypeVector(
+                                            EMBEDDING_INDEXING_LINEAR_SEARCH)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("propEmbed1")
+                                        .SetDataTypeVector(
+                                            EMBEDDING_INDEXING_LINEAR_SEARCH)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+  // SectionIds are assigned in ascending order per schema type,
+  // alphabetically.
+  int email_bar_section_id = 0;
+  int email_foo_section_id = 1;
+  int email_prop_embed0_section_id = 2;
+  int email_prop_embed1_section_id = 3;
+  ASSERT_THAT(schema_store_->SetSchema(
+                  schema, /*ignore_errors_and_delete_documents=*/false),
+              IsOk());
+
+  // These documents don't actually match to the tokens in the index. We're
+  // inserting the documents to get the appropriate number of documents and
+  // schema types populated.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
+  DocumentId email_document_id0 = put_result0.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
+  DocumentId email_document_id1 = put_result1.new_document_id;
+
+  // Poplate the index
+  TermMatchType::Code term_match_type = TermMatchType::EXACT_ONLY;
+
+  // Email1 has content "animal" in section foo
+  ASSERT_THAT(AddTokenToIndex(email_document_id0, email_foo_section_id,
+                              term_match_type, "animal"),
+              IsOk());
+
+  // Email2 has content "animal" in section bar
+  ASSERT_THAT(AddTokenToIndex(email_document_id1, email_bar_section_id,
+                              term_match_type, "animal"),
+              IsOk());
+
+  // Add embedding vectors into different sections for the two documents.
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(email_prop_embed0_section_id, email_document_id0),
+      CreateVector("my_model1", {1, -2, -3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"email"));
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(email_prop_embed1_section_id, email_document_id0),
+      CreateVector("my_model1", {-1, -2, 3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"email"));
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(email_prop_embed0_section_id, email_document_id1),
+      CreateVector("my_model1", {-1, 2, 3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"email"));
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(email_prop_embed1_section_id, email_document_id1),
+      CreateVector("my_model1", {1, 2, -3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"email"));
+  ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
+
+  // Create an embedding query with semantic scores:
+  // - document 0: -2 (section 0), 6 (section 1)
+  // - document 1: 2 (section 0), -6 (section 1)
+  PropertyProto::VectorProto embedding_query_vector =
+      CreateVector("my_model1", {-1, -1, 1});
+
+  SearchSpecProto search_spec;
+  std::string query = "semanticSearch(getEmbeddingParameter(0), 0) AND animal";
+  search_spec.set_query(query);
+  search_spec.set_term_match_type(term_match_type);
+  search_spec.set_embedding_query_metric_type(
+      SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT);
+  search_spec.add_enabled_features(kListFilterQueryLanguageFeature);
+  *search_spec.add_embedding_query_vectors() =
+      std::move(embedding_query_vector);
+
+  // Add property filters to match both text sections but only the first
+  // embedding section. Therefore, only the second document should be returned.
+  TypePropertyMask* email_mask = search_spec.add_type_property_filters();
+  email_mask->set_schema_type("email");
+  email_mask->add_paths("foo");
+  email_mask->add_paths("bar");
+  email_mask->add_paths("propEmbed0");
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      QueryResults results,
+      query_processor_->ParseSearch(
+          search_spec, ScoringSpecProto::RankingStrategy::RELEVANCE_SCORE,
+          /*get_embedding_match_info=*/false,
+          fake_clock_.GetSystemTimeMilliseconds()));
+
+  // Ordered by descending DocumentId, so message comes first since it was
+  // inserted last
+  DocHitInfo expected_doc_hit_info1(email_document_id1);
+  expected_doc_hit_info1.UpdateSection(email_bar_section_id);
+  expected_doc_hit_info1.UpdateSection(email_prop_embed0_section_id);
+  EXPECT_THAT(GetDocHitInfos(results.root_iterator.get()),
+              ElementsAre(expected_doc_hit_info1));
+  EXPECT_THAT(results.query_term_iterators, SizeIs(1));
+
+  EXPECT_THAT(results.query_terms, SizeIs(1));
+  EXPECT_THAT(results.query_terms[""], UnorderedElementsAre("animal"));
+
+  // The results should be the same even if the order is flipped.
+  query = "animal AND semanticSearch(getEmbeddingParameter(0), 0)";
+  search_spec.set_query(query);
+  ICING_ASSERT_OK_AND_ASSIGN(
+      results,
+      query_processor_->ParseSearch(
+          search_spec, ScoringSpecProto::RankingStrategy::RELEVANCE_SCORE,
+          /*get_embedding_match_info=*/false,
+          fake_clock_.GetSystemTimeMilliseconds()));
+
+  // Ordered by descending DocumentId, so message comes first since it was
+  // inserted last
+  EXPECT_THAT(GetDocHitInfos(results.root_iterator.get()),
+              ElementsAre(expected_doc_hit_info1));
+  EXPECT_THAT(results.query_term_iterators, SizeIs(1));
+
+  EXPECT_THAT(results.query_terms, SizeIs(1));
+  EXPECT_THAT(results.query_terms[""], UnorderedElementsAre("animal"));
+}
+
+TEST_F(QueryProcessorTest, TypePropertyFilterHybridSearchRestrictEmbedding) {
+  // Create the schema and document store
+  // Create the schema and document store
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("foo")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("bar")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("propEmbed0")
+                                        .SetDataTypeVector(
+                                            EMBEDDING_INDEXING_LINEAR_SEARCH)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("propEmbed1")
+                                        .SetDataTypeVector(
+                                            EMBEDDING_INDEXING_LINEAR_SEARCH)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+  // SectionIds are assigned in ascending order per schema type,
+  // alphabetically.
+  int email_bar_section_id = 0;
+  int email_foo_section_id = 1;
+  int email_prop_embed0_section_id = 2;
+  int email_prop_embed1_section_id = 3;
+  ASSERT_THAT(schema_store_->SetSchema(
+                  schema, /*ignore_errors_and_delete_documents=*/false),
+              IsOk());
+
+  // These documents don't actually match to the tokens in the index. We're
+  // inserting the documents to get the appropriate number of documents and
+  // schema types populated.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
+  DocumentId email_document_id0 = put_result0.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
+  DocumentId email_document_id1 = put_result1.new_document_id;
+
+  // Poplate the index
+  TermMatchType::Code term_match_type = TermMatchType::EXACT_ONLY;
+
+  // Email1 has content "animal" in section foo
+  ASSERT_THAT(AddTokenToIndex(email_document_id0, email_foo_section_id,
+                              term_match_type, "animal"),
+              IsOk());
+
+  // Email2 has content "animal" in section bar
+  ASSERT_THAT(AddTokenToIndex(email_document_id1, email_bar_section_id,
+                              term_match_type, "animal"),
+              IsOk());
+
+  // Add embedding vectors into different sections for the two documents.
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(email_prop_embed0_section_id, email_document_id0),
+      CreateVector("my_model1", {1, -2, -3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"email"));
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(email_prop_embed1_section_id, email_document_id0),
+      CreateVector("my_model1", {-1, -2, 3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"email"));
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(email_prop_embed0_section_id, email_document_id1),
+      CreateVector("my_model1", {-1, 2, 3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"email"));
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(email_prop_embed1_section_id, email_document_id1),
+      CreateVector("my_model1", {1, 2, -3}), QUANTIZATION_TYPE_NONE,
+      /*schema_name=*/"email"));
+  ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
+
+  // Create an embedding query with semantic scores:
+  // - document 0: -2 (section 0), 6 (section 1)
+  // - document 1: 2 (section 0), -6 (section 1)
+  PropertyProto::VectorProto embedding_query_vector =
+      CreateVector("my_model1", {-1, -1, 1});
+
+  SearchSpecProto search_spec;
+  std::string query = "semanticSearch(getEmbeddingParameter(0), 0) AND animal";
+  search_spec.set_query(query);
+  search_spec.set_term_match_type(term_match_type);
+  search_spec.set_embedding_query_metric_type(
+      SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT);
+  search_spec.add_enabled_features(kListFilterQueryLanguageFeature);
+  *search_spec.add_embedding_query_vectors() =
+      std::move(embedding_query_vector);
+
+  // Add property filters to match both embeddings sections but only the first
+  // text section. Therefore, only the first document should be returned.
+  TypePropertyMask* email_mask = search_spec.add_type_property_filters();
+  email_mask->set_schema_type("email");
+  email_mask->add_paths("foo");
+  email_mask->add_paths("propEmbed0");
+  email_mask->add_paths("propEmbed1");
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      QueryResults results,
+      query_processor_->ParseSearch(
+          search_spec, ScoringSpecProto::RankingStrategy::RELEVANCE_SCORE,
+          /*get_embedding_match_info=*/false,
+          fake_clock_.GetSystemTimeMilliseconds()));
+
+  // Ordered by descending DocumentId, so message comes first since it was
+  // inserted last
+  DocHitInfo expected_doc_hit_info1(email_document_id0);
+  expected_doc_hit_info1.UpdateSection(email_foo_section_id);
+  expected_doc_hit_info1.UpdateSection(email_prop_embed1_section_id);
+  EXPECT_THAT(GetDocHitInfos(results.root_iterator.get()),
+              ElementsAre(expected_doc_hit_info1));
+  EXPECT_THAT(results.query_term_iterators, SizeIs(1));
+
+  EXPECT_THAT(results.query_terms, SizeIs(1));
+  EXPECT_THAT(results.query_terms[""], UnorderedElementsAre("animal"));
+
+  // The results should be the same even if the order is flipped.
+  query = "animal AND semanticSearch(getEmbeddingParameter(0), 0)";
+  search_spec.set_query(query);
+  ICING_ASSERT_OK_AND_ASSIGN(
+      results,
+      query_processor_->ParseSearch(
+          search_spec, ScoringSpecProto::RankingStrategy::RELEVANCE_SCORE,
+          /*get_embedding_match_info=*/false,
+          fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(GetDocHitInfos(results.root_iterator.get()),
+              ElementsAre(expected_doc_hit_info1));
+  EXPECT_THAT(results.query_term_iterators, SizeIs(1));
+
+  EXPECT_THAT(results.query_terms, SizeIs(1));
+  EXPECT_THAT(results.query_terms[""], UnorderedElementsAre("animal"));
+}
+
 TEST_F(QueryProcessorTest, DocumentBeforeTtlNotFilteredOut) {
   // Create the schema and document store
   SchemaProto schema = SchemaBuilder()
@@ -2979,12 +3407,13 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(DocumentBuilder()
-                               .SetKey("namespace", "1")
-                               .SetSchema("email")
-                               .SetCreationTimestampMs(10)
-                               .SetTtlMs(100)
-                               .Build()));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .SetCreationTimestampMs(10)
+                                                   .SetTtlMs(100)
+                                                   .Build())));
   DocumentId document_id = put_result.new_document_id;
 
   // Populate the index
@@ -3044,12 +3473,13 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(DocumentBuilder()
-                               .SetKey("namespace", "1")
-                               .SetSchema("email")
-                               .SetCreationTimestampMs(50)
-                               .SetTtlMs(100)
-                               .Build()));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .SetCreationTimestampMs(50)
+                                                   .SetTtlMs(100)
+                                                   .Build())));
   DocumentId document_id = put_result.new_document_id;
 
   // Populate the index
@@ -3107,33 +3537,36 @@
               IsOk());
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(DocumentBuilder()
-                               .SetKey("namespace", "1")
-                               .SetSchema("transaction")
-                               .AddInt64Property("price", 10)
-                               .Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder()
+              .SetKey("namespace", "1")
+              .SetSchema("transaction")
+              .AddInt64Property("price", 10)
+              .Build())));
   DocumentId document_one_id = put_result1.new_document_id;
   ICING_ASSERT_OK(
       AddToNumericIndex(document_one_id, "price", price_section_id, 10));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(DocumentBuilder()
-                               .SetKey("namespace", "2")
-                               .SetSchema("transaction")
-                               .AddInt64Property("price", 25)
-                               .Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder()
+              .SetKey("namespace", "2")
+              .SetSchema("transaction")
+              .AddInt64Property("price", 25)
+              .Build())));
   DocumentId document_two_id = put_result2.new_document_id;
   ICING_ASSERT_OK(
       AddToNumericIndex(document_two_id, "price", price_section_id, 25));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
-      document_store_->Put(DocumentBuilder()
-                               .SetKey("namespace", "3")
-                               .SetSchema("transaction")
-                               .AddInt64Property("cost", 2)
-                               .Build()));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "3")
+                                                   .SetSchema("transaction")
+                                                   .AddInt64Property("cost", 2)
+                                                   .Build())));
   DocumentId document_three_id = put_result3.new_document_id;
   ICING_ASSERT_OK(
       AddToNumericIndex(document_three_id, "cost", cost_section_id, 2));
@@ -3212,11 +3645,12 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(DocumentBuilder()
-                               .SetKey("namespace", "1")
-                               .SetSchema("transaction")
-                               .AddInt64Property("price", 10)
-                               .Build()));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          DocumentBuilder()
+              .SetKey("namespace", "1")
+              .SetSchema("transaction")
+              .AddInt64Property("price", 10)
+              .Build())));
   DocumentId document_one_id = put_result1.new_document_id;
   ICING_ASSERT_OK(
       AddToNumericIndex(document_one_id, "price", price_section_id, 10));
@@ -3268,11 +3702,13 @@
   //   Doc2:
   //     prop1: "foo bar"
   //     prop2: ""
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "0")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "0")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id0 = put_result0.new_document_id;
   EXPECT_THAT(
       AddTokenToIndex(document_id0, prop1_section_id, term_match_type, "foo"),
@@ -3281,11 +3717,13 @@
       AddTokenToIndex(document_id0, prop2_section_id, term_match_type, "bar"),
       IsOk());
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id1 = put_result1.new_document_id;
   EXPECT_THAT(
       AddTokenToIndex(document_id1, prop1_section_id, term_match_type, "bar"),
@@ -3294,11 +3732,13 @@
       AddTokenToIndex(document_id1, prop2_section_id, term_match_type, "foo"),
       IsOk());
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id2 = put_result2.new_document_id;
   EXPECT_THAT(
       AddTokenToIndex(document_id2, prop1_section_id, term_match_type, "foo"),
@@ -3387,11 +3827,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId document_id = put_result.new_document_id;
 
   // Populate the index
@@ -3436,10 +3878,11 @@
   ASSERT_THAT(schema_store_->SetSchema(
                   schema, /*ignore_errors_and_delete_documents=*/false),
               IsOk());
-  ICING_ASSERT_OK(document_store_->Put(DocumentBuilder()
-                                           .SetKey("namespace", "uri1")
-                                           .SetSchema("email")
-                                           .Build()));
+  ICING_ASSERT_OK(document_store_->Put(
+      document_util::CreateDocumentWrapper(DocumentBuilder()
+                                               .SetKey("namespace", "uri1")
+                                               .SetSchema("email")
+                                               .Build())));
 
   SearchSpecProto search_spec;
   search_spec.set_term_match_type(TermMatchType::PREFIX);
diff --git a/icing/query/query-utils.cc b/icing/query/query-utils.cc
index 0a4cbd3..5cd8d16 100644
--- a/icing/query/query-utils.cc
+++ b/icing/query/query-utils.cc
@@ -14,13 +14,16 @@
 
 #include "icing/query/query-utils.h"
 
+#include <cstdint>
+#include <memory>
 #include <string_view>
 #include <unordered_set>
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
-#include "icing/index/iterator/doc-hit-info-iterator-filter.h"
+#include "icing/index/iterator/document-filter-predicate.h"
 #include "icing/schema/schema-store.h"
 #include "icing/store/document-filter-data.h"
+#include "icing/store/document-id.h"
 #include "icing/store/document-store.h"
 #include "icing/store/namespace-id.h"
 
@@ -59,25 +62,85 @@
   return ids;
 }
 
+class DocumentFilterPredicateBySchemaAndNamespace
+    : public DocumentFilterPredicate {
+ public:
+  DocumentFilterPredicateBySchemaAndNamespace(
+      const SearchSpecProto& search_spec, const DocumentStore& document_store,
+      const SchemaStore& schema_store, int64_t current_time_ms)
+      : document_store_(document_store), current_time_ms_(current_time_ms) {
+    // Precompute all the NamespaceIds
+    filter_by_namespace_id_enabled = !search_spec.namespace_filters().empty();
+    target_namespace_ids = ConvertNamespaceToIds(document_store, search_spec);
+
+    // Precompute all the SchemaTypeIds
+    filter_by_schema_type_id_enabled =
+        !search_spec.schema_type_filters().empty();
+    target_schema_type_ids =
+        ConvertExactSchemaTypeToIds(schema_store, search_spec);
+  }
+
+  bool operator()(DocumentId document_id) const override {
+    // Try to get the DocumentFilterData
+    auto document_filter_data = document_store_.GetAliveDocumentFilterData(
+        document_id, current_time_ms_);
+    if (!document_filter_data) {
+      // Didn't find the DocumentFilterData in the filter cache. This could be
+      // because the Document doesn't exist or the DocumentId isn't valid or the
+      // filter cache is in some invalid state. This is bad, but not the query's
+      // responsibility to fix, so just skip this result for now.
+      return false;
+    }
+    // We should be guaranteed that filter data exists now.
+    if (filter_by_namespace_id_enabled &&
+        target_namespace_ids.count(document_filter_data->namespace_id()) == 0) {
+      // Doesn't match one of the specified namespaces.
+      return false;
+    }
+
+    if (filter_by_schema_type_id_enabled &&
+        target_schema_type_ids.count(document_filter_data->schema_type_id()) ==
+            0) {
+      // Doesn't match one of the specified schema types.
+      return false;
+    }
+
+    return true;
+  }
+
+ private:
+  // List of namespace ids that documents must have.
+  // filter_by_namespace_id_enabled=false means that all namespaces are valid,
+  // and no documents will be filtered out.
+  //
+  // Note that if we want to reference the strings in namespaces later, ensure
+  // that the caller who passed the Options class outlives the
+  // DocHitInfoIteratorFilter.
+  std::unordered_set<NamespaceId> target_namespace_ids;
+
+  // List of schema type ids that documents must have.
+  // filter_by_schema_type_id_enabled=false means that all schema types are
+  // valid, and no documents will be filtered out.
+  //
+  // Note that if we want to reference the strings in schema types later,
+  // ensure that the caller who passed the Options class outlives the
+  // DocHitInfoIteratorFilter.
+  std::unordered_set<SchemaTypeId> target_schema_type_ids;
+
+  bool filter_by_schema_type_id_enabled = false;
+  bool filter_by_namespace_id_enabled = false;
+
+  const DocumentStore& document_store_;
+  int64_t current_time_ms_;
+};
+
 }  // namespace
 
-DocHitInfoIteratorFilter::Options GetFilterOptions(
+std::unique_ptr<DocumentFilterPredicate> GetFilterPredicateBySchemaAndNamespace(
     const SearchSpecProto& search_spec, const DocumentStore& document_store,
-    const SchemaStore& schema_store) {
-  DocHitInfoIteratorFilter::Options options;
-
-  // Precompute all the NamespaceIds
-  options.filter_by_namespace_id_enabled =
-      !search_spec.namespace_filters().empty();
-  options.target_namespace_ids =
-      ConvertNamespaceToIds(document_store, search_spec);
-
-  // Precompute all the SchemaTypeIds
-  options.filter_by_schema_type_id_enabled =
-      !search_spec.schema_type_filters().empty();
-  options.target_schema_type_ids =
-      ConvertExactSchemaTypeToIds(schema_store, search_spec);
-  return options;
+    const SchemaStore& schema_store, int64_t current_time_ms) {
+  return std::make_unique<DocumentFilterPredicateBySchemaAndNamespace>(
+      search_spec, document_store, schema_store, current_time_ms);
 }
 
 }  // namespace lib
diff --git a/icing/query/query-utils.h b/icing/query/query-utils.h
index dae75a2..6c18a39 100644
--- a/icing/query/query-utils.h
+++ b/icing/query/query-utils.h
@@ -15,7 +15,10 @@
 #ifndef ICING_QUERY_QUERY_UTILS_H_
 #define ICING_QUERY_QUERY_UTILS_H_
 
-#include "icing/index/iterator/doc-hit-info-iterator-filter.h"
+#include <cstdint>
+#include <memory>
+
+#include "icing/index/iterator/document-filter-predicate.h"
 #include "icing/proto/search.pb.h"
 #include "icing/schema/schema-store.h"
 #include "icing/store/document-store.h"
@@ -23,9 +26,9 @@
 namespace icing {
 namespace lib {
 
-DocHitInfoIteratorFilter::Options GetFilterOptions(
+std::unique_ptr<DocumentFilterPredicate> GetFilterPredicateBySchemaAndNamespace(
     const SearchSpecProto& search_spec, const DocumentStore& document_store,
-    const SchemaStore& schema_store);
+    const SchemaStore& schema_store, int64_t current_time_ms);
 
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/query/suggestion-processor_test.cc b/icing/query/suggestion-processor_test.cc
index 567eeae..2ebaf08 100644
--- a/icing/query/suggestion-processor_test.cc
+++ b/icing/query/suggestion-processor_test.cc
@@ -35,6 +35,7 @@
 #include "icing/index/term-metadata.h"
 #include "icing/jni/jni-cache.h"
 #include "icing/legacy/index/icing-filesystem.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/query/query-features.h"
 #include "icing/schema-builder.h"
@@ -50,9 +51,10 @@
 #include "icing/testing/tmp-directory.h"
 #include "icing/tokenization/language-segmenter-factory.h"
 #include "icing/tokenization/language-segmenter.h"
-#include "icing/transform/normalizer-options.h"
 #include "icing/transform/normalizer-factory.h"
+#include "icing/transform/normalizer-options.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/document-util.h"
 #include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
@@ -110,14 +112,18 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, store_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
 
     Index::Options options(index_dir_,
@@ -125,7 +131,8 @@
                            /*lite_index_sort_at_indexing=*/true,
                            /*lite_index_sort_size=*/1024 * 8);
     ICING_ASSERT_OK_AND_ASSIGN(
-        index_, Index::Create(options, &filesystem_, &icing_filesystem_));
+        index_, Index::Create(options, &filesystem_, &icing_filesystem_,
+                              feature_flags_.get()));
     // TODO(b/249829533): switch to use persistent numeric index.
     ICING_ASSERT_OK_AND_ASSIGN(
         numeric_index_,
@@ -133,7 +140,8 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         embedding_index_,
         EmbeddingIndex::Create(&filesystem_, embedding_index_dir_, &fake_clock_,
-                               feature_flags_.get()));
+                               feature_flags_.get(),
+                               /*num_shards=*/32));
 
     language_segmenter_factory::SegmenterOptions segmenter_options(
         ULOC_US, jni_cache_.get());
@@ -208,17 +216,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId0 = put_result0.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId1 = put_result1.new_document_id;
 
   ASSERT_THAT(AddTokenToIndex(documentId0, kSectionId2,
@@ -256,17 +268,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId0 = put_result0.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId1 = put_result1.new_document_id;
 
   ASSERT_THAT(AddTokenToIndex(documentId0, kSectionId2,
@@ -308,17 +324,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId0 = put_result0.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId1 = put_result1.new_document_id;
 
   ASSERT_THAT(AddTokenToIndex(documentId0, kSectionId2,
@@ -362,23 +382,29 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId0 = put_result0.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "3")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "3")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId2 = put_result2.new_document_id;
 
   ASSERT_THAT(AddTokenToIndex(documentId0, kSectionId2,
@@ -430,17 +456,21 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId0 = put_result0.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "2")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "2")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId1 = put_result1.new_document_id;
 
   ASSERT_THAT(AddTokenToIndex(documentId0, kSectionId2,
@@ -493,11 +523,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId0 = put_result0.new_document_id;
 
   ASSERT_THAT(AddTokenToIndex(documentId0, kSectionId2,
@@ -529,11 +561,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId0 = put_result0.new_document_id;
 
   ASSERT_THAT(AddTokenToIndex(documentId0, kSectionId2,
@@ -566,11 +600,13 @@
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId0 = put_result0.new_document_id;
   ASSERT_THAT(AddTokenToIndex(documentId0, kSectionId2,
                               TermMatchType::EXACT_ONLY, "foo"),
@@ -619,11 +655,13 @@
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId0 = put_result0.new_document_id;
   ASSERT_THAT(AddTokenToIndex(documentId0, kSectionId2,
                               TermMatchType::EXACT_ONLY, "foo"),
@@ -666,11 +704,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId0 = put_result0.new_document_id;
 
   ASSERT_THAT(AddTokenToIndex(documentId0, kSectionId2,
@@ -729,11 +769,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId0 = put_result0.new_document_id;
 
   ASSERT_THAT(AddTokenToIndex(documentId0, kSectionId2,
@@ -778,11 +820,13 @@
   // These documents don't actually match to the tokens in the index. We're
   // inserting the documents to get the appropriate number of documents and
   // namespaces populated.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
-                             document_store_->Put(DocumentBuilder()
-                                                      .SetKey("namespace1", "1")
-                                                      .SetSchema("email")
-                                                      .Build()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result0,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(DocumentBuilder()
+                                                   .SetKey("namespace1", "1")
+                                                   .SetSchema("email")
+                                                   .Build())));
   DocumentId documentId0 = put_result0.new_document_id;
 
   ASSERT_THAT(AddTokenToIndex(documentId0, kSectionId2,
diff --git a/icing/result/result-adjustment-info.cc b/icing/result/result-adjustment-info.cc
index 6ae911c..82ffb0b 100644
--- a/icing/result/result-adjustment-info.cc
+++ b/icing/result/result-adjustment-info.cc
@@ -29,7 +29,9 @@
 #include "icing/result/snippet-context.h"
 #include "icing/schema/schema-store.h"
 #include "icing/schema/section.h"
+#include "icing/scoring/advanced_scoring/double-list.h"
 #include "icing/store/document-id.h"
+#include "icing/util/embedding-util.h"
 #include "icing/util/logging.h"
 
 namespace icing {
@@ -47,45 +49,57 @@
     const std::unordered_set<DocumentId>& documents_to_include,
     int num_matches_per_property) {
   SnippetContext::DocumentEmbeddingMatchInfoMap result_map;
+  if (embedding_query_results.global_section_infos->empty()) {
+    // No section info indicates that embedding match info is not
+    // enabled.
+    return result_map;
+  }
+  if (embedding_query_results.global_section_infos->size() !=
+      embedding_query_results.global_scores->size()) {
+    // This should never happen.
+    ICING_LOG(ERROR) << "EmbeddingMatchInfos has mismatched section_infos and "
+                        "scores vectors. Global section_infos size: "
+                     << embedding_query_results.global_section_infos->size()
+                     << ", Global scores size: "
+                     << embedding_query_results.global_scores->size();
+    return result_map;
+  }
+
   // Maps from (document_id, section_id) to the match count for that section.
   std::unordered_map<DocumentId, std::unordered_map<SectionId, int>>
       section_match_count;
 
-  for (const auto& [query_vector_index, metric_type_map] :
-       embedding_query_results.result_infos) {
-    for (const auto& [metric_type, info_map] : metric_type_map) {
-      for (const auto& [doc_id, match_infos] : info_map) {
+  for (int query_vector_index = 0;
+       query_vector_index < embedding_query_results.GetNumQueryVectors();
+       ++query_vector_index) {
+    for (SearchSpecProto::EmbeddingQueryMetricType::Code metric_type :
+         embedding_util::kEmbeddingQueryMetricTypes) {
+      const EmbeddingQueryResults::EmbeddingQueryMatchInfoMap* info_map =
+          embedding_query_results.GetMatchInfoMap(query_vector_index,
+                                                  metric_type);
+      if (info_map == nullptr) {
+        continue;
+      }
+      for (const auto& [doc_id, match_infos] : *info_map) {
         if (documents_to_include.find(doc_id) == documents_to_include.end()) {
           continue;
         }
 
-        if (match_infos.section_infos == nullptr) {
-          // No section info indicates that embedding match info is not
-          // enabled.
-          continue;
-        }
-
-        if (match_infos.section_infos->size() != match_infos.scores.size()) {
-          // This should never happen.
-          ICING_LOG(ERROR)
-              << "EmbeddingMatchInfos has mismatched section_infos and "
-                 "scores vectors for document with id "
-              << doc_id
-              << ". Section_infos size: " << match_infos.section_infos->size()
-              << ", scores size: " << match_infos.scores.size();
-          continue;
-        }
-        for (int i = 0; i < match_infos.scores.size(); ++i) {
-          SectionId section_id = match_infos.section_infos->at(i).section_id;
-          if (section_match_count[doc_id][section_id] >=
+        DoubleList scores =
+            embedding_query_results.GetMatchedScoresFromEmbeddingMatchInfos(
+                match_infos);
+        for (int i = 0; i < scores.size(); ++i) {
+          const EmbeddingMatchInfos::EmbeddingMatchSectionInfo& section_info =
+              embedding_query_results.global_section_infos->at(
+                  i + match_infos.score_start_index);
+          if (section_match_count[doc_id][section_info.section_id] >=
               num_matches_per_property) {
             continue;
           }
           result_map[doc_id].push_back(SnippetContext::EmbeddingMatchInfoEntry(
-              match_infos.scores[i], metric_type,
-              match_infos.section_infos->at(i).position, query_vector_index,
-              section_id));
-          ++section_match_count[doc_id][section_id];
+              scores.data()[i], metric_type, section_info.position,
+              query_vector_index, section_info.section_id));
+          ++section_match_count[doc_id][section_info.section_id];
         }
       }
     }
diff --git a/icing/result/result-adjustment-info_test.cc b/icing/result/result-adjustment-info_test.cc
index d539016..0c0c841 100644
--- a/icing/result/result-adjustment-info_test.cc
+++ b/icing/result/result-adjustment-info_test.cc
@@ -147,49 +147,67 @@
       CreateSearchSpec(TermMatchType::EXACT_ONLY, embedding_query_vectors,
                        EMBEDDING_METRIC_DOT_PRODUCT);
 
-  EmbeddingQueryResults embedding_query_results;
+  EmbeddingQueryResults embedding_query_results(/*num_query_vectors=*/2);
   EmbeddingMatchInfos& info_query0_doc0 =
-      embedding_query_results
-          .result_infos[/*query_index=*/0]
-                       [search_spec.embedding_query_metric_type()]
-                       [kDocumentId0];
-  info_query0_doc0.AppendScore(1);
-  info_query0_doc0.AppendScore(1.7);
-  info_query0_doc0.AppendScore(3.3);
-  info_query0_doc0.AppendSectionInfo(/*section_id=*/0, /*position=*/0);
-  info_query0_doc0.AppendSectionInfo(/*section_id=*/0, /*position=*/3);
-  info_query0_doc0.AppendSectionInfo(/*section_id=*/1, /*position=*/1);
+      GetOrCreateEmbeddingMatchInfosForDocument(
+          embedding_query_results, /*query_index=*/0,
+          search_spec.embedding_query_metric_type(), kDocumentId0);
+  info_query0_doc0.AppendScore(*embedding_query_results.global_scores, 1);
+  info_query0_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/0,
+      /*position=*/0);
+  info_query0_doc0.AppendScore(*embedding_query_results.global_scores, 1.7);
+  info_query0_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/0,
+      /*position=*/3);
+  info_query0_doc0.AppendScore(*embedding_query_results.global_scores, 3.3);
+  info_query0_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/1,
+      /*position=*/1);
   EmbeddingMatchInfos& info_query1_doc0 =
-      embedding_query_results
-          .result_infos[/*query_index=*/1]
-                       [search_spec.embedding_query_metric_type()]
-                       [kDocumentId0];
-  info_query1_doc0.AppendScore(2);
-  info_query1_doc0.AppendScore(1.7);
-  info_query1_doc0.AppendSectionInfo(/*section_id=*/0, /*position=*/0);
-  info_query1_doc0.AppendSectionInfo(/*section_id=*/3, /*position=*/2);
+      GetOrCreateEmbeddingMatchInfosForDocument(
+          embedding_query_results, /*query_index=*/1,
+          search_spec.embedding_query_metric_type(), kDocumentId0);
+  info_query1_doc0.AppendScore(*embedding_query_results.global_scores, 2);
+  info_query1_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/0,
+      /*position=*/0);
+  info_query1_doc0.AppendScore(*embedding_query_results.global_scores, 1.7);
+  info_query1_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/3,
+      /*position=*/2);
   EmbeddingMatchInfos& info_query1_doc1 =
-      embedding_query_results
-          .result_infos[/*query_index=*/1][EMBEDDING_METRIC_COSINE]
-                       [kDocumentId1];
-  info_query1_doc1.AppendScore(6.66);
-  info_query1_doc1.AppendSectionInfo(/*section_id=*/1, /*position=*/0);
+      GetOrCreateEmbeddingMatchInfosForDocument(
+          embedding_query_results, /*query_index=*/1, EMBEDDING_METRIC_COSINE,
+          kDocumentId1);
+  info_query1_doc1.AppendScore(*embedding_query_results.global_scores, 6.66);
+  info_query1_doc1.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/1,
+      /*position=*/0);
   EmbeddingMatchInfos& info_query0_doc2 =
-      embedding_query_results
-          .result_infos[/*query_index=*/0][EMBEDDING_METRIC_COSINE]
-                       [kDocumentId2];
-  info_query0_doc2.AppendScore(5.25);
-  info_query0_doc2.AppendSectionInfo(/*section_id=*/1, /*position=*/0);
-  info_query0_doc2.AppendScore(1.33);
-  info_query0_doc2.AppendSectionInfo(/*section_id=*/1, /*position=*/4);
+      GetOrCreateEmbeddingMatchInfosForDocument(
+          embedding_query_results, /*query_index=*/0, EMBEDDING_METRIC_COSINE,
+          kDocumentId2);
+  info_query0_doc2.AppendScore(*embedding_query_results.global_scores, 5.25);
+  info_query0_doc2.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/1,
+      /*position=*/0);
+  info_query0_doc2.AppendScore(*embedding_query_results.global_scores, 1.33);
+  info_query0_doc2.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/1,
+      /*position=*/4);
   EmbeddingMatchInfos& info_query1_doc3 =
-      embedding_query_results
-          .result_infos[/*query_index=*/1][EMBEDDING_METRIC_COSINE]
-                       [kDocumentId3];
-  info_query1_doc3.AppendScore(3.25);
-  info_query1_doc3.AppendSectionInfo(/*section_id=*/1, /*position=*/1);
-  info_query1_doc3.AppendScore(2.33);
-  info_query1_doc3.AppendSectionInfo(/*section_id=*/1, /*position=*/2);
+      GetOrCreateEmbeddingMatchInfosForDocument(
+          embedding_query_results, /*query_index=*/1, EMBEDDING_METRIC_COSINE,
+          kDocumentId3);
+  info_query1_doc3.AppendScore(*embedding_query_results.global_scores, 3.25);
+  info_query1_doc3.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/1,
+      /*position=*/1);
+  info_query1_doc3.AppendScore(*embedding_query_results.global_scores, 2.33);
+  info_query1_doc3.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/1,
+      /*position=*/2);
 
   ResultAdjustmentInfo result_adjustment_info(
       search_spec, CreateScoringSpec(/*is_descending_order=*/true), result_spec,
@@ -309,49 +327,67 @@
       CreateSearchSpec(TermMatchType::EXACT_ONLY, embedding_query_vectors,
                        EMBEDDING_METRIC_DOT_PRODUCT);
 
-  EmbeddingQueryResults embedding_query_results;
+  EmbeddingQueryResults embedding_query_results(/*num_query_vectors=*/2);
   EmbeddingMatchInfos& info_query0_doc0 =
-      embedding_query_results
-          .result_infos[/*query_index=*/0]
-                       [search_spec.embedding_query_metric_type()]
-                       [kDocumentId0];
-  info_query0_doc0.AppendScore(1);
-  info_query0_doc0.AppendScore(1.7);
-  info_query0_doc0.AppendScore(3.3);
-  info_query0_doc0.AppendSectionInfo(/*section_id=*/0, /*position=*/0);
-  info_query0_doc0.AppendSectionInfo(/*section_id=*/0, /*position=*/3);
-  info_query0_doc0.AppendSectionInfo(/*section_id=*/1, /*position=*/1);
+      GetOrCreateEmbeddingMatchInfosForDocument(
+          embedding_query_results, /*query_index=*/0,
+          search_spec.embedding_query_metric_type(), kDocumentId0);
+  info_query0_doc0.AppendScore(*embedding_query_results.global_scores, 1);
+  info_query0_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/0,
+      /*position=*/0);
+  info_query0_doc0.AppendScore(*embedding_query_results.global_scores, 1.7);
+  info_query0_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/0,
+      /*position=*/3);
+  info_query0_doc0.AppendScore(*embedding_query_results.global_scores, 3.3);
+  info_query0_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/1,
+      /*position=*/1);
   EmbeddingMatchInfos& info_query1_doc0 =
-      embedding_query_results
-          .result_infos[/*query_index=*/1]
-                       [search_spec.embedding_query_metric_type()]
-                       [kDocumentId0];
-  info_query1_doc0.AppendScore(2);
-  info_query1_doc0.AppendScore(1.7);
-  info_query1_doc0.AppendSectionInfo(/*section_id=*/0, /*position=*/0);
-  info_query1_doc0.AppendSectionInfo(/*section_id=*/3, /*position=*/2);
+      GetOrCreateEmbeddingMatchInfosForDocument(
+          embedding_query_results, /*query_index=*/1,
+          search_spec.embedding_query_metric_type(), kDocumentId0);
+  info_query1_doc0.AppendScore(*embedding_query_results.global_scores, 2);
+  info_query1_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/0,
+      /*position=*/0);
+  info_query1_doc0.AppendScore(*embedding_query_results.global_scores, 1.7);
+  info_query1_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/3,
+      /*position=*/2);
   EmbeddingMatchInfos& info_query1_doc1 =
-      embedding_query_results
-          .result_infos[/*query_index=*/1][EMBEDDING_METRIC_COSINE]
-                       [kDocumentId1];
-  info_query1_doc1.AppendScore(6.66);
-  info_query1_doc1.AppendSectionInfo(/*section_id=*/1, /*position=*/0);
+      GetOrCreateEmbeddingMatchInfosForDocument(
+          embedding_query_results, /*query_index=*/1, EMBEDDING_METRIC_COSINE,
+          kDocumentId1);
+  info_query1_doc1.AppendScore(*embedding_query_results.global_scores, 6.66);
+  info_query1_doc1.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/1,
+      /*position=*/0);
   EmbeddingMatchInfos& info_query0_doc2 =
-      embedding_query_results
-          .result_infos[/*query_index=*/0][EMBEDDING_METRIC_COSINE]
-                       [kDocumentId2];
-  info_query0_doc2.AppendScore(5.25);
-  info_query0_doc2.AppendSectionInfo(/*section_id=*/1, /*position=*/0);
-  info_query0_doc2.AppendScore(1.33);
-  info_query0_doc2.AppendSectionInfo(/*section_id=*/1, /*position=*/4);
+      GetOrCreateEmbeddingMatchInfosForDocument(
+          embedding_query_results, /*query_index=*/0, EMBEDDING_METRIC_COSINE,
+          kDocumentId2);
+  info_query0_doc2.AppendScore(*embedding_query_results.global_scores, 5.25);
+  info_query0_doc2.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/1,
+      /*position=*/0);
+  info_query0_doc2.AppendScore(*embedding_query_results.global_scores, 1.33);
+  info_query0_doc2.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/1,
+      /*position=*/4);
   EmbeddingMatchInfos& info_query1_doc3 =
-      embedding_query_results
-          .result_infos[/*query_index=*/1][EMBEDDING_METRIC_COSINE]
-                       [kDocumentId3];
-  info_query1_doc3.AppendScore(3.25);
-  info_query1_doc3.AppendSectionInfo(/*section_id=*/1, /*position=*/1);
-  info_query1_doc3.AppendScore(2.33);
-  info_query1_doc3.AppendSectionInfo(/*section_id=*/1, /*position=*/2);
+      GetOrCreateEmbeddingMatchInfosForDocument(
+          embedding_query_results, /*query_index=*/1, EMBEDDING_METRIC_COSINE,
+          kDocumentId3);
+  info_query1_doc3.AppendScore(*embedding_query_results.global_scores, 3.25);
+  info_query1_doc3.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/1,
+      /*position=*/1);
+  info_query1_doc3.AppendScore(*embedding_query_results.global_scores, 2.33);
+  info_query1_doc3.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/1,
+      /*position=*/2);
 
   ResultAdjustmentInfo result_adjustment_info(
       search_spec, CreateScoringSpec(/*is_descending_order=*/true), result_spec,
@@ -383,8 +419,8 @@
                                     /*section_id=*/0)),
                             EqualsEmbeddingMatchInfoEntry(
                                 SnippetContext::EmbeddingMatchInfoEntry(
-                                    /*score=*/2, EMBEDDING_METRIC_DOT_PRODUCT,
-                                    /*position=*/0, /*query_vector_index=*/1,
+                                    /*score=*/1.7, EMBEDDING_METRIC_DOT_PRODUCT,
+                                    /*position=*/3, /*query_vector_index=*/0,
                                     /*section_id=*/0)),
                             EqualsEmbeddingMatchInfoEntry(
                                 SnippetContext::EmbeddingMatchInfoEntry(
@@ -422,33 +458,43 @@
   SearchSpecProto search_spec =
       CreateSearchSpec(TermMatchType::EXACT_ONLY, embedding_query_vectors,
                        EMBEDDING_METRIC_DOT_PRODUCT);
-  EmbeddingQueryResults embedding_query_results;
+  EmbeddingQueryResults embedding_query_results(/*num_query_vectors=*/2);
   EmbeddingMatchInfos& info_query0_doc0 =
-      embedding_query_results
-          .result_infos[/*query_index=*/0]
-                       [search_spec.embedding_query_metric_type()]
-                       [kDocumentId0];
-  info_query0_doc0.AppendScore(1);
-  info_query0_doc0.AppendScore(1.7);
-  info_query0_doc0.AppendScore(3.3);
-  info_query0_doc0.AppendSectionInfo(/*section_id=*/0, /*position=*/0);
-  info_query0_doc0.AppendSectionInfo(/*section_id=*/0, /*position=*/3);
-  info_query0_doc0.AppendSectionInfo(/*section_id=*/1, /*position=*/1);
+      GetOrCreateEmbeddingMatchInfosForDocument(
+          embedding_query_results, /*query_index=*/0,
+          search_spec.embedding_query_metric_type(), kDocumentId0);
+  info_query0_doc0.AppendScore(*embedding_query_results.global_scores, 1);
+  info_query0_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/0,
+      /*position=*/0);
+  info_query0_doc0.AppendScore(*embedding_query_results.global_scores, 1.7);
+  info_query0_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/0,
+      /*position=*/3);
+  info_query0_doc0.AppendScore(*embedding_query_results.global_scores, 3.3);
+  info_query0_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/1,
+      /*position=*/1);
   EmbeddingMatchInfos& info_query1_doc0 =
-      embedding_query_results
-          .result_infos[/*query_index=*/1]
-                       [search_spec.embedding_query_metric_type()]
-                       [kDocumentId0];
-  info_query1_doc0.AppendScore(2);
-  info_query1_doc0.AppendScore(1.7);
-  info_query1_doc0.AppendSectionInfo(/*section_id=*/0, /*position=*/0);
-  info_query1_doc0.AppendSectionInfo(/*section_id=*/3, /*position=*/2);
+      GetOrCreateEmbeddingMatchInfosForDocument(
+          embedding_query_results, /*query_index=*/1,
+          search_spec.embedding_query_metric_type(), kDocumentId0);
+  info_query1_doc0.AppendScore(*embedding_query_results.global_scores, 2);
+  info_query1_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/0,
+      /*position=*/0);
+  info_query1_doc0.AppendScore(*embedding_query_results.global_scores, 1.7);
+  info_query1_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/3,
+      /*position=*/2);
   EmbeddingMatchInfos& info_query1_doc1 =
-      embedding_query_results
-          .result_infos[/*query_index=*/1][EMBEDDING_METRIC_COSINE]
-                       [kDocumentId1];
-  info_query1_doc1.AppendScore(6.66);
-  info_query1_doc1.AppendSectionInfo(/*section_id=*/1, /*position=*/0);
+      GetOrCreateEmbeddingMatchInfosForDocument(
+          embedding_query_results, /*query_index=*/1, EMBEDDING_METRIC_COSINE,
+          kDocumentId1);
+  info_query1_doc1.AppendScore(*embedding_query_results.global_scores, 6.66);
+  info_query1_doc1.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/1,
+      /*position=*/0);
 
   ResultAdjustmentInfo result_adjustment_info(
       search_spec, CreateScoringSpec(/*is_descending_order=*/true), result_spec,
@@ -526,33 +572,43 @@
   SearchSpecProto search_spec =
       CreateSearchSpec(TermMatchType::EXACT_ONLY, embedding_query_vectors,
                        SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT);
-  EmbeddingQueryResults embedding_query_results;
+  EmbeddingQueryResults embedding_query_results(/*num_query_vectors=*/2);
   EmbeddingMatchInfos& info_query0_doc0 =
-      embedding_query_results
-          .result_infos[/*query_index=*/0]
-                       [search_spec.embedding_query_metric_type()]
-                       [kDocumentId0];
-  info_query0_doc0.AppendScore(1);
-  info_query0_doc0.AppendScore(1.7);
-  info_query0_doc0.AppendScore(3.3);
-  info_query0_doc0.AppendSectionInfo(/*section_id=*/0, /*position=*/0);
-  info_query0_doc0.AppendSectionInfo(/*section_id=*/0, /*position=*/3);
-  info_query0_doc0.AppendSectionInfo(/*section_id=*/1, /*position=*/1);
+      GetOrCreateEmbeddingMatchInfosForDocument(
+          embedding_query_results, /*query_index=*/0,
+          search_spec.embedding_query_metric_type(), kDocumentId0);
+  info_query0_doc0.AppendScore(*embedding_query_results.global_scores, 1);
+  info_query0_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/0,
+      /*position=*/0);
+  info_query0_doc0.AppendScore(*embedding_query_results.global_scores, 1.7);
+  info_query0_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/0,
+      /*position=*/3);
+  info_query0_doc0.AppendScore(*embedding_query_results.global_scores, 3.3);
+  info_query0_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/1,
+      /*position=*/1);
   EmbeddingMatchInfos& info_query1_doc0 =
-      embedding_query_results
-          .result_infos[/*query_index=*/1]
-                       [search_spec.embedding_query_metric_type()]
-                       [kDocumentId0];
-  info_query1_doc0.AppendScore(2);
-  info_query1_doc0.AppendScore(1.7);
-  info_query1_doc0.AppendSectionInfo(/*section_id=*/0, /*position=*/0);
-  info_query1_doc0.AppendSectionInfo(/*section_id=*/3, /*position=*/2);
+      GetOrCreateEmbeddingMatchInfosForDocument(
+          embedding_query_results, /*query_index=*/1,
+          search_spec.embedding_query_metric_type(), kDocumentId0);
+  info_query1_doc0.AppendScore(*embedding_query_results.global_scores, 2);
+  info_query1_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/0,
+      /*position=*/0);
+  info_query1_doc0.AppendScore(*embedding_query_results.global_scores, 1.7);
+  info_query1_doc0.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/3,
+      /*position=*/2);
   EmbeddingMatchInfos& info_query1_doc1 =
-      embedding_query_results
-          .result_infos[/*query_index=*/1][EMBEDDING_METRIC_COSINE]
-                       [kDocumentId1];
-  info_query1_doc1.AppendScore(6.66);
-  info_query1_doc1.AppendSectionInfo(/*section_id=*/1, /*position=*/0);
+      GetOrCreateEmbeddingMatchInfosForDocument(
+          embedding_query_results, /*query_index=*/1, EMBEDDING_METRIC_COSINE,
+          kDocumentId1);
+  info_query1_doc1.AppendScore(*embedding_query_results.global_scores, 6.66);
+  info_query1_doc1.AppendSectionInfo(
+      *embedding_query_results.global_section_infos, /*section_id=*/1,
+      /*position=*/0);
 
   ResultAdjustmentInfo result_adjustment_info(
       search_spec, CreateScoringSpec(/*is_descending_order=*/true), result_spec,
diff --git a/icing/result/result-retriever-v2.cc b/icing/result/result-retriever-v2.cc
index 4d221f3..3d5104b 100644
--- a/icing/result/result-retriever-v2.cc
+++ b/icing/result/result-retriever-v2.cc
@@ -14,9 +14,11 @@
 
 #include "icing/result/result-retriever-v2.h"
 
+#include <algorithm>
 #include <cstddef>
 #include <cstdint>
 #include <memory>
+#include <optional>
 #include <string>
 #include <unordered_map>
 #include <utility>
@@ -24,6 +26,7 @@
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/mutex.h"
+#include "icing/feature-flags.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/search.pb.h"
 #include "icing/result/page-result.h"
@@ -97,52 +100,55 @@
 
 }  // namespace
 
-bool GroupResultLimiterV2::ShouldBeRemoved(
+std::optional<int> GroupResultLimiterV2::GetGroupResultLimitsIndex(
     const ScoredDocumentHit& scored_document_hit,
     const std::unordered_map<int32_t, int>& entry_id_group_id_map,
-    const DocumentStore& document_store, std::vector<int>& group_result_limits,
+    const DocumentStore& document_store,
     ResultSpecProto::ResultGroupingType result_group_type,
     int64_t current_time_ms) const {
+  if (result_group_type == ResultSpecProto::ResultGroupingType::
+                               ResultSpecProto_ResultGroupingType_NONE) {
+    // No limit. Return -1 to skip the group result limit.
+    return -1;
+  }
+
   auto document_filter_data_optional =
       document_store.GetAliveDocumentFilterData(
           scored_document_hit.document_id(), current_time_ms);
   if (!document_filter_data_optional) {
-    // The document doesn't exist.
-    return true;
+    // The document doesn't exist. Return std::nullopt to exclude the document.
+    return std::nullopt;
   }
-  NamespaceId namespace_id =
-      document_filter_data_optional.value().namespace_id();
-  SchemaTypeId schema_type_id =
-      document_filter_data_optional.value().schema_type_id();
-  auto entry_id_or = document_store.GetResultGroupingEntryId(
+  NamespaceId namespace_id = document_filter_data_optional->namespace_id();
+  SchemaTypeId schema_type_id = document_filter_data_optional->schema_type_id();
+
+  std::optional<int32_t> entry_id = document_store.GetResultGroupingEntryId(
       result_group_type, namespace_id, schema_type_id);
-  if (!entry_id_or.ok()) {
-    return false;
+  if (!entry_id.has_value()) {
+    // No limit. Return -1 to skip the group result limit.
+    return -1;
   }
-  int32_t entry_id = entry_id_or.ValueOrDie();
-  auto iter = entry_id_group_id_map.find(entry_id);
+
+  auto iter = entry_id_group_id_map.find(*entry_id);
   if (iter == entry_id_group_id_map.end()) {
     // If a ResultGrouping Entry Id isn't found in entry_id_group_id_map, then
     // there are no limits placed on results from this entry id.
-    return false;
+    return -1;
   }
-  int& count = group_result_limits.at(iter->second);
-  if (count <= 0) {
-    return true;
-  }
-  --count;
-  return false;
+  return iter->second;
 }
 
 libtextclassifier3::StatusOr<std::unique_ptr<ResultRetrieverV2>>
 ResultRetrieverV2::Create(
     const DocumentStore* doc_store, const SchemaStore* schema_store,
     const LanguageSegmenter* language_segmenter, const Normalizer* normalizer,
+    const FeatureFlags* feature_flags,
     std::unique_ptr<const GroupResultLimiterV2> group_result_limiter) {
   ICING_RETURN_ERROR_IF_NULL(doc_store);
   ICING_RETURN_ERROR_IF_NULL(schema_store);
   ICING_RETURN_ERROR_IF_NULL(language_segmenter);
   ICING_RETURN_ERROR_IF_NULL(normalizer);
+  ICING_RETURN_ERROR_IF_NULL(feature_flags);
   ICING_RETURN_ERROR_IF_NULL(group_result_limiter);
 
   ICING_ASSIGN_OR_RETURN(
@@ -151,11 +157,12 @@
 
   return std::unique_ptr<ResultRetrieverV2>(
       new ResultRetrieverV2(doc_store, std::move(snippet_retriever),
-                            std::move(group_result_limiter)));
+                            std::move(group_result_limiter), feature_flags));
 }
 
 std::pair<PageResult, bool> ResultRetrieverV2::RetrieveNextPage(
-    ResultStateV2& result_state, int64_t current_time_ms) const {
+    ResultStateV2& result_state, int32_t max_results,
+    int64_t current_time_ms) const {
   absl_ports::unique_lock l(&result_state.mutex);
 
   // For calculating page
@@ -166,103 +173,49 @@
   // Retrieve info
   std::vector<SearchResultProto::ResultProto> results;
   int32_t num_total_bytes = 0;
-  while (results.size() < result_state.num_per_page() &&
+  int desired_page_size = std::min(result_state.num_per_page(), max_results);
+  while (num_total_bytes < result_state.num_total_bytes_per_page_threshold() &&
+         results.size() < desired_page_size &&
          !result_state.scored_document_hits_ranker->empty()) {
-    JoinedScoredDocumentHit next_best_document_hit =
-        result_state.scored_document_hits_ranker->PopNext();
-    if (group_result_limiter_->ShouldBeRemoved(
-            next_best_document_hit.parent_scored_document_hit(),
-            result_state.entry_id_group_id_map(), doc_store_,
-            result_state.group_result_limits, result_state.result_group_type(),
-            current_time_ms)) {
-      continue;
-    }
+    RetrieveResult result = Retrieve(result_state, current_time_ms);
+    if (result.result_proto.has_value()) {
+      if (result.has_parent_snippets) {
+        ++num_results_with_snippets;
+      }
 
-    DocumentId doc_id =
-        next_best_document_hit.parent_scored_document_hit().document_id();
-    libtextclassifier3::StatusOr<DocumentProto> document_or =
-        doc_store_.Get(doc_id);
-    if (!document_or.ok()) {
-      // Skip the document if getting errors.
-      ICING_LOG(WARNING) << "Fail to fetch document from document store: "
-                         << document_or.status().error_message();
-      continue;
-    }
-
-    DocumentProto document = std::move(document_or).ValueOrDie();
-    // Apply parent projection
-    ApplyProjection(result_state.parent_adjustment_info(), &document);
-
-    SearchResultProto::ResultProto result;
-    // Add parent snippet if requested.
-    if (ApplySnippet(result_state.parent_adjustment_info(), *snippet_retriever_,
-                     document, doc_id,
-                     next_best_document_hit.parent_scored_document_hit()
-                         .hit_section_id_mask(),
-                     &result)) {
-      ++num_results_with_snippets;
-    }
-
-    // Add the document, itself.
-    *result.mutable_document() = std::move(document);
-    result.set_score(next_best_document_hit.final_score());
-    const auto* parent_additional_scores =
-        next_best_document_hit.parent_scored_document_hit().additional_scores();
-    if (parent_additional_scores != nullptr) {
-      result.mutable_additional_scores()->Add(parent_additional_scores->begin(),
-                                              parent_additional_scores->end());
-    }
-
-    // Retrieve child documents
-    for (const ScoredDocumentHit& child_scored_document_hit :
-         next_best_document_hit.child_scored_document_hits()) {
-      if (result.joined_results_size() >=
-          result_state.max_joined_children_per_parent_to_return()) {
+      // Apply byte size threshold enforcement only if it is not the first
+      // document. This ensures that at least one document is returned,
+      // otherwise it will get stuck forever since nothing is popped from the
+      // ranker.
+      //
+      // (Use subtraction to avoid integer overflow).
+      size_t result_bytes = result.result_proto->ByteSizeLong();
+      if (feature_flags_.enable_strict_page_byte_size_limit() &&
+          !results.empty() &&
+          result_bytes >= result_state.num_total_bytes_per_page_threshold() -
+                              num_total_bytes) {
+        // Exceeds the byte size threshold, so skip the current document. Also
+        // it remains in the ranker and will be included in the next page.
+        ICING_LOG(INFO) << "Skipping document due to byte size threshold. "
+                           "Current num docs: "
+                        << results.size()
+                        << ", total byte size: " << num_total_bytes
+                        << ", next doc byte size: " << result_bytes
+                        << ", threshold: "
+                        << result_state.num_total_bytes_per_page_threshold();
         break;
       }
 
-      DocumentId child_doc_id = child_scored_document_hit.document_id();
-      libtextclassifier3::StatusOr<DocumentProto> child_document_or =
-          doc_store_.Get(child_doc_id);
-      if (!child_document_or.ok()) {
-        // Skip the document if getting errors.
-        ICING_LOG(WARNING)
-            << "Fail to fetch child document from document store: "
-            << child_document_or.status().error_message();
-        continue;
-      }
+      results.push_back(std::move(*result.result_proto));
+      num_total_bytes += result_bytes;
 
-      DocumentProto child_document = std::move(child_document_or).ValueOrDie();
-      ApplyProjection(result_state.child_adjustment_info(), &child_document);
-
-      SearchResultProto::ResultProto* child_result =
-          result.add_joined_results();
-      // Add child snippet if requested.
-      ApplySnippet(result_state.child_adjustment_info(), *snippet_retriever_,
-                   child_document, child_doc_id,
-                   child_scored_document_hit.hit_section_id_mask(),
-                   child_result);
-
-      *child_result->mutable_document() = std::move(child_document);
-      child_result->set_score(child_scored_document_hit.score());
-      if (child_scored_document_hit.additional_scores() != nullptr) {
-        child_result->mutable_additional_scores()->Add(
-            child_scored_document_hit.additional_scores()->begin(),
-            child_scored_document_hit.additional_scores()->end());
+      // Decrement the counter of the group result limit in ResultState.
+      if (result.group_result_limits_index != -1) {
+        --result_state.group_result_limits[result.group_result_limits_index];
       }
     }
 
-    size_t result_bytes = result.ByteSizeLong();
-    results.push_back(std::move(result));
-
-    // Check if num_total_bytes + result_bytes reaches or exceeds
-    // num_total_bytes_per_page_threshold. Use subtraction to avoid integer
-    // overflow.
-    if (result_bytes >=
-        result_state.num_total_bytes_per_page_threshold() - num_total_bytes) {
-      break;
-    }
-    num_total_bytes += result_bytes;
+    result_state.scored_document_hits_ranker->Pop();
   }
 
   // Update numbers in ResultState
@@ -279,5 +232,113 @@
       has_more_results);
 }
 
+ResultRetrieverV2::RetrieveResult ResultRetrieverV2::Retrieve(
+    ResultStateV2& result_state, int64_t current_time_ms) const {
+  const JoinedScoredDocumentHit& next_best_document_hit =
+      result_state.scored_document_hits_ranker->Top();
+
+  // Get the index of the group result limits for the given scored document hit,
+  // and check if the document should be excluded.
+  std::optional<int> idx = group_result_limiter_->GetGroupResultLimitsIndex(
+      next_best_document_hit.parent_scored_document_hit(),
+      result_state.entry_id_group_id_map(), doc_store_,
+      result_state.result_group_type(), current_time_ms);
+  if (!idx.has_value()) {
+    // Should exclude the document, so return an invalid result.
+    return {.result_proto = std::nullopt,
+            .group_result_limits_index = -1,
+            .has_parent_snippets = false};
+  }
+
+  if (*idx == -1) {
+    // No limit for the result document. Pass.
+  } else if (*idx < 0 || *idx >= result_state.group_result_limits.size()) {
+    // This should not happen. But let's check it anyway and log. Also set the
+    // index to -1 indicating that there is no limit.
+    ICING_LOG(WARNING) << "Get an invalid group result limits index: " << *idx;
+    *idx = -1;
+  } else if (result_state.group_result_limits[*idx] <= 0) {
+    // Should exclude the document since the group has no budget left, so
+    // return an invalid result.
+    return {.result_proto = std::nullopt,
+            .group_result_limits_index = -1,
+            .has_parent_snippets = false};
+  }
+
+  DocumentId doc_id =
+      next_best_document_hit.parent_scored_document_hit().document_id();
+  auto document_or = doc_store_.Get(doc_id);
+  if (!document_or.ok()) {
+    // Exclude the document if getting errors.
+    ICING_LOG(WARNING) << "Fail to fetch document from document store: "
+                       << document_or.status().error_message();
+    return {.result_proto = std::nullopt,
+            .group_result_limits_index = -1,
+            .has_parent_snippets = false};
+  }
+  DocumentProto document = std::move(document_or).ValueOrDie();
+
+  // Apply parent projection
+  ApplyProjection(result_state.parent_adjustment_info(), &document);
+
+  SearchResultProto::ResultProto result;
+  // Add parent snippet if requested.
+  bool has_parent_snippets = ApplySnippet(
+      result_state.parent_adjustment_info(), *snippet_retriever_, document,
+      doc_id,
+      next_best_document_hit.parent_scored_document_hit().hit_section_id_mask(),
+      &result);
+
+  // Add the document, itself.
+  *result.mutable_document() = std::move(document);
+  result.set_score(next_best_document_hit.final_score());
+  const auto* parent_additional_scores =
+      next_best_document_hit.parent_scored_document_hit().additional_scores();
+  if (parent_additional_scores != nullptr) {
+    result.mutable_additional_scores()->Add(parent_additional_scores->begin(),
+                                            parent_additional_scores->end());
+  }
+
+  // Retrieve child documents
+  for (const ScoredDocumentHit& child_scored_document_hit :
+       next_best_document_hit.child_scored_document_hits()) {
+    if (result.joined_results_size() >=
+        result_state.max_joined_children_per_parent_to_return()) {
+      break;
+    }
+
+    DocumentId child_doc_id = child_scored_document_hit.document_id();
+    libtextclassifier3::StatusOr<DocumentProto> child_document_or =
+        doc_store_.Get(child_doc_id);
+    if (!child_document_or.ok()) {
+      // Skip the document if getting errors.
+      ICING_LOG(WARNING) << "Fail to fetch child document from document store: "
+                         << child_document_or.status().error_message();
+      continue;
+    }
+
+    DocumentProto child_document = std::move(child_document_or).ValueOrDie();
+    ApplyProjection(result_state.child_adjustment_info(), &child_document);
+
+    SearchResultProto::ResultProto* child_result = result.add_joined_results();
+    // Add child snippet if requested.
+    ApplySnippet(result_state.child_adjustment_info(), *snippet_retriever_,
+                 child_document, child_doc_id,
+                 child_scored_document_hit.hit_section_id_mask(), child_result);
+
+    *child_result->mutable_document() = std::move(child_document);
+    child_result->set_score(child_scored_document_hit.score());
+    if (child_scored_document_hit.additional_scores() != nullptr) {
+      child_result->mutable_additional_scores()->Add(
+          child_scored_document_hit.additional_scores()->begin(),
+          child_scored_document_hit.additional_scores()->end());
+    }
+  }
+
+  return RetrieveResult{.result_proto = std::make_optional(std::move(result)),
+                        .group_result_limits_index = *idx,
+                        .has_parent_snippets = has_parent_snippets};
+}
+
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/result/result-retriever-v2.h b/icing/result/result-retriever-v2.h
index 7b1a364..0516feb 100644
--- a/icing/result/result-retriever-v2.h
+++ b/icing/result/result-retriever-v2.h
@@ -17,11 +17,13 @@
 
 #include <cstdint>
 #include <memory>
+#include <optional>
 #include <unordered_map>
 #include <utility>
-#include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/absl_ports/thread_annotations.h"
+#include "icing/feature-flags.h"
 #include "icing/proto/search.pb.h"
 #include "icing/result/page-result.h"
 #include "icing/result/result-state-v2.h"
@@ -41,12 +43,18 @@
 
   virtual ~GroupResultLimiterV2() = default;
 
-  // Returns true if the scored_document_hit should be removed.
-  virtual bool ShouldBeRemoved(
+  // Gets the index of the group result limits for the given scored document
+  // hit.
+  //
+  // Returns:
+  //   - A valid index of the group result limits.
+  //   - -1 indicating that there is no limit for the result document.
+  //   - std::nullopt if the result document, its namespace, or its schema type
+  //     is not present. The caller should exclude the document from the page.
+  virtual std::optional<int> GetGroupResultLimitsIndex(
       const ScoredDocumentHit& scored_document_hit,
       const std::unordered_map<int32_t, int>& entry_id_group_id_map,
       const DocumentStore& document_store,
-      std::vector<int>& group_result_limits,
       ResultSpecProto::ResultGroupingType result_group_type,
       int64_t current_time_ms) const;
 };
@@ -63,7 +71,7 @@
   static libtextclassifier3::StatusOr<std::unique_ptr<ResultRetrieverV2>>
   Create(const DocumentStore* doc_store, const SchemaStore* schema_store,
          const LanguageSegmenter* language_segmenter,
-         const Normalizer* normalizer,
+         const Normalizer* normalizer, const FeatureFlags* feature_flags,
          std::unique_ptr<const GroupResultLimiterV2> group_result_limiter =
              std::make_unique<const GroupResultLimiterV2>());
 
@@ -72,8 +80,8 @@
   // out the next top rank documents from ResultState, retrieves the documents
   // from storage, updates ResultState, and finally wraps the result + other
   // information into PageResult. The expected number of documents to return is
-  // min(num_per_page, the number of all scored document hits) inside
-  // ResultState.
+  // min(max_results, num_per_page, the number of all scored document hits)
+  // inside ResultState.
   //
   // The number of snippets to return is based on the total number of snippets
   // needed and number of snippets that have already been returned previously
@@ -89,20 +97,54 @@
   // Returns:
   //   std::pair<PageResult, bool>
   std::pair<PageResult, bool> RetrieveNextPage(ResultStateV2& result_state,
-                                               int64_t current_time_ms) const;
+                                               int32_t max_results,
+                                               int64_t current_time_ms) const
+      ICING_LOCKS_EXCLUDED(result_state.mutex);
 
  private:
   explicit ResultRetrieverV2(
       const DocumentStore* doc_store,
       std::unique_ptr<SnippetRetriever> snippet_retriever,
-      std::unique_ptr<const GroupResultLimiterV2> group_result_limiter)
+      std::unique_ptr<const GroupResultLimiterV2> group_result_limiter,
+      const FeatureFlags* feature_flags)
       : doc_store_(*doc_store),
         snippet_retriever_(std::move(snippet_retriever)),
-        group_result_limiter_(std::move(group_result_limiter)) {}
+        group_result_limiter_(std::move(group_result_limiter)),
+        feature_flags_(*feature_flags) {}
+
+  // Helper function to construct a ResultProto by the next best document hit
+  // from the scored document hits ranker.
+  //
+  // REQUIRES: !result_state.scored_document_hits_ranker.empty()
+  struct RetrieveResult {
+    // The constructed result proto. If std::nullopt, then the document should
+    // be skipped.
+    std::optional<SearchResultProto::ResultProto> result_proto;
+
+    // The index of the group result limits for the result. The caller should
+    // decrement the corresponding result limit in
+    // result_state.group_result_limits after deciding to include the result
+    // document in the page.
+    // - It is guaranteed to be -1 or in the range of [0,
+    //   result_state.group_result_limits.size() - 1]. If it is -1, then it
+    //   means there is no limit for the result document.
+    // - Only used when the proto is not std::nullopt.
+    int group_result_limits_index;
+
+    // Whether the (parent) document of the result has snippets. Only used when
+    // the proto is not std::nullopt.
+    bool has_parent_snippets;
+  };
+
+  RetrieveResult Retrieve(ResultStateV2& result_state,
+                          int64_t current_time_ms) const
+      ICING_EXCLUSIVE_LOCKS_REQUIRED(result_state.mutex);
 
   const DocumentStore& doc_store_;
   std::unique_ptr<SnippetRetriever> snippet_retriever_;
   const std::unique_ptr<const GroupResultLimiterV2> group_result_limiter_;
+
+  const FeatureFlags& feature_flags_;
 };
 
 }  // namespace lib
diff --git a/icing/result/result-retriever-v2_group-result-limiter_test.cc b/icing/result/result-retriever-v2_group-result-limiter_test.cc
index 964e92e..50160c1 100644
--- a/icing/result/result-retriever-v2_group-result-limiter_test.cc
+++ b/icing/result/result-retriever-v2_group-result-limiter_test.cc
@@ -15,13 +15,18 @@
 #include <cstdint>
 #include <limits>
 #include <memory>
+#include <string>
+#include <utility>
 #include <vector>
 
 #include "gtest/gtest.h"
+#include "icing/absl_ports/mutex.h"
 #include "icing/document-builder.h"
 #include "icing/feature-flags.h"
+#include "icing/file/filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/portable/equals-proto.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
@@ -41,9 +46,11 @@
 #include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 #include "icing/tokenization/language-segmenter-factory.h"
+#include "icing/tokenization/language-segmenter.h"
 #include "icing/transform/normalizer-factory.h"
 #include "icing/transform/normalizer-options.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/document-util.h"
 #include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
@@ -99,14 +106,18 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, test_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, test_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
   }
 
@@ -142,8 +153,9 @@
                                 .SetScore(1)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document2 = DocumentBuilder()
@@ -152,8 +164,9 @@
                                 .SetScore(2)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   std::vector<ScoredDocumentHit> scored_document_hits = {
@@ -181,12 +194,14 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Only the top ranked document in "namespace" (document2), should be
   // returned.
   auto [page_result, has_more_results] = result_retriever->RetrieveNextPage(
-      result_state, fake_clock_.GetSystemTimeMilliseconds());
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
   ASSERT_THAT(page_result.results, SizeIs(1));
   EXPECT_THAT(page_result.results.at(0).document(), EqualsProto(document2));
   // Document1 has not been returned due to GroupResultLimiter, but since it was
@@ -204,8 +219,9 @@
                                 .SetScore(1)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document2 = DocumentBuilder()
@@ -214,8 +230,9 @@
                                 .SetScore(2)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   std::vector<ScoredDocumentHit> scored_document_hits = {
@@ -243,11 +260,13 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // First page: empty page
   auto [page_result, has_more_results] = result_retriever->RetrieveNextPage(
-      result_state, fake_clock_.GetSystemTimeMilliseconds());
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
   ASSERT_THAT(page_result.results, IsEmpty());
   EXPECT_FALSE(has_more_results);
 }
@@ -262,8 +281,9 @@
                                 .SetScore(1)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document2 = DocumentBuilder()
@@ -272,8 +292,9 @@
                                 .SetScore(2)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   DocumentProto document3 = DocumentBuilder()
@@ -282,8 +303,9 @@
                                 .SetScore(3)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store_->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store_->Put(document_util::CreateDocumentWrapper(document3)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   DocumentProto document4 = DocumentBuilder()
@@ -292,8 +314,9 @@
                                 .SetScore(4)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             document_store_->Put(document4));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      document_store_->Put(document_util::CreateDocumentWrapper(document4)));
   DocumentId document_id4 = put_result4.new_document_id;
 
   std::vector<ScoredDocumentHit> scored_document_hits = {
@@ -323,11 +346,13 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // First page: document4 and document3 should be returned.
   auto [page_result1, has_more_results1] = result_retriever->RetrieveNextPage(
-      result_state, fake_clock_.GetSystemTimeMilliseconds());
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
   ASSERT_THAT(page_result1.results, SizeIs(2));
   EXPECT_THAT(page_result1.results.at(0).document(), EqualsProto(document4));
   EXPECT_THAT(page_result1.results.at(1).document(), EqualsProto(document3));
@@ -337,7 +362,8 @@
   // them will be filtered out by group result limiter, so we should get an
   // empty page.
   auto [page_result2, has_more_results2] = result_retriever->RetrieveNextPage(
-      result_state, fake_clock_.GetSystemTimeMilliseconds());
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
   EXPECT_THAT(page_result2.results, SizeIs(0));
   EXPECT_FALSE(has_more_results2);
 }
@@ -352,8 +378,9 @@
                                 .SetScore(1)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document2 = DocumentBuilder()
@@ -362,8 +389,9 @@
                                 .SetScore(2)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   DocumentProto document3 = DocumentBuilder()
@@ -372,8 +400,9 @@
                                 .SetScore(3)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store_->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store_->Put(document_util::CreateDocumentWrapper(document3)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   DocumentProto document4 = DocumentBuilder()
@@ -382,8 +411,9 @@
                                 .SetScore(4)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             document_store_->Put(document4));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      document_store_->Put(document_util::CreateDocumentWrapper(document4)));
   DocumentId document_id4 = put_result4.new_document_id;
 
   std::vector<ScoredDocumentHit> scored_document_hits = {
@@ -414,13 +444,15 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // All documents in "namespace2" should be returned.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(3));
   EXPECT_THAT(page_result.results.at(0).document(), EqualsProto(document4));
@@ -438,8 +470,9 @@
                                 .SetScore(1)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document2 = DocumentBuilder()
@@ -448,8 +481,9 @@
                                 .SetScore(2)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   std::vector<ScoredDocumentHit> scored_document_hits = {
@@ -480,15 +514,17 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Only the top ranked document in "namespace" (document2), should be
   // returned. The presence of "nonexistentNamespace" in the same result
   // grouping should have no effect.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(1));
   EXPECT_THAT(page_result.results.at(0).document(), EqualsProto(document2));
@@ -504,8 +540,9 @@
                                 .SetScore(1)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document2 = DocumentBuilder()
@@ -514,8 +551,9 @@
                                 .SetScore(2)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   std::vector<ScoredDocumentHit> scored_document_hits = {
@@ -546,15 +584,17 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Only the top ranked document in "Document" (document2), should be
   // returned. The presence of "nonexistentNamespace" in the same result
   // grouping should have no effect.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(1));
   EXPECT_THAT(page_result.results.at(0).document(), EqualsProto(document2));
@@ -571,8 +611,9 @@
                                 .SetScore(1)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document2 = DocumentBuilder()
@@ -581,8 +622,9 @@
                                 .SetScore(2)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   DocumentProto document3 = DocumentBuilder()
@@ -591,8 +633,9 @@
                                 .SetScore(3)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store_->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store_->Put(document_util::CreateDocumentWrapper(document3)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   DocumentProto document4 = DocumentBuilder()
@@ -601,8 +644,9 @@
                                 .SetScore(4)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             document_store_->Put(document4));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      document_store_->Put(document_util::CreateDocumentWrapper(document4)));
   DocumentId document_id4 = put_result4.new_document_id;
 
   DocumentProto document5 = DocumentBuilder()
@@ -611,8 +655,9 @@
                                 .SetScore(5)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result5,
-                             document_store_->Put(document5));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result5,
+      document_store_->Put(document_util::CreateDocumentWrapper(document5)));
   DocumentId document_id5 = put_result5.new_document_id;
 
   DocumentProto document6 = DocumentBuilder()
@@ -621,8 +666,9 @@
                                 .SetScore(6)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result6,
-                             document_store_->Put(document6));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result6,
+      document_store_->Put(document_util::CreateDocumentWrapper(document6)));
   DocumentId document_id6 = put_result6.new_document_id;
 
   std::vector<ScoredDocumentHit> scored_document_hits = {
@@ -661,15 +707,17 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Only the top-ranked result in "namespace1" (document2) should be returned.
   // Only the top-ranked results across "namespace2" and "namespace3"
   // (document6, document5) should be returned.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(3));
   EXPECT_THAT(page_result.results.at(0).document(), EqualsProto(document6));
@@ -688,8 +736,9 @@
                                 .SetScore(1)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document2 = DocumentBuilder()
@@ -698,8 +747,9 @@
                                 .SetScore(2)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   DocumentProto document3 = DocumentBuilder()
@@ -708,8 +758,9 @@
                                 .SetScore(3)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store_->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store_->Put(document_util::CreateDocumentWrapper(document3)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   DocumentProto document4 = DocumentBuilder()
@@ -718,8 +769,9 @@
                                 .SetScore(4)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             document_store_->Put(document4));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      document_store_->Put(document_util::CreateDocumentWrapper(document4)));
   DocumentId document_id4 = put_result4.new_document_id;
 
   DocumentProto document5 = DocumentBuilder()
@@ -728,8 +780,9 @@
                                 .SetScore(5)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result5,
-                             document_store_->Put(document5));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result5,
+      document_store_->Put(document_util::CreateDocumentWrapper(document5)));
   DocumentId document_id5 = put_result5.new_document_id;
 
   DocumentProto document6 = DocumentBuilder()
@@ -738,8 +791,9 @@
                                 .SetScore(6)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result6,
-                             document_store_->Put(document6));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result6,
+      document_store_->Put(document_util::CreateDocumentWrapper(document6)));
   DocumentId document_id6 = put_result6.new_document_id;
 
   std::vector<ScoredDocumentHit> scored_document_hits = {
@@ -778,15 +832,17 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Only the top-ranked result in "Document" (document6) should be returned.
   // Only the top-ranked results across "Message" and "Person"
   // (document5, document3) should be returned.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(3));
   EXPECT_THAT(page_result.results.at(0).document(), EqualsProto(document6));
@@ -805,8 +861,9 @@
                                 .SetScore(1)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document2 = DocumentBuilder()
@@ -815,8 +872,9 @@
                                 .SetScore(2)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   DocumentProto document3 = DocumentBuilder()
@@ -825,8 +883,9 @@
                                 .SetScore(3)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store_->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store_->Put(document_util::CreateDocumentWrapper(document3)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   DocumentProto document4 = DocumentBuilder()
@@ -835,8 +894,9 @@
                                 .SetScore(4)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             document_store_->Put(document4));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      document_store_->Put(document_util::CreateDocumentWrapper(document4)));
   DocumentId document_id4 = put_result4.new_document_id;
 
   DocumentProto document5 = DocumentBuilder()
@@ -845,8 +905,9 @@
                                 .SetScore(5)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result5,
-                             document_store_->Put(document5));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result5,
+      document_store_->Put(document_util::CreateDocumentWrapper(document5)));
   DocumentId document_id5 = put_result5.new_document_id;
 
   DocumentProto document6 = DocumentBuilder()
@@ -855,8 +916,9 @@
                                 .SetScore(6)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result6,
-                             document_store_->Put(document6));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result6,
+      document_store_->Put(document_util::CreateDocumentWrapper(document6)));
   DocumentId document_id6 = put_result6.new_document_id;
 
   std::vector<ScoredDocumentHit> scored_document_hits = {
@@ -898,7 +960,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Only the top-ranked result in "namespace1xDocument" (document3)
   // should be returned.
@@ -907,8 +970,9 @@
 
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(3));
   EXPECT_THAT(page_result.results.at(0).document(), EqualsProto(document6));
@@ -926,8 +990,9 @@
                                 .SetScore(1)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document2 = DocumentBuilder()
@@ -936,8 +1001,9 @@
                                 .SetScore(2)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   std::vector<ScoredDocumentHit> scored_document_hits = {
@@ -966,14 +1032,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // All documents in "namespace" should be returned. The presence of
   // "nonexistentNamespace" should have no effect.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
   EXPECT_THAT(page_result.results.at(0).document(), EqualsProto(document2));
@@ -990,8 +1058,9 @@
                                 .SetScore(1)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document2 = DocumentBuilder()
@@ -1000,8 +1069,9 @@
                                 .SetScore(2)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   std::vector<ScoredDocumentHit> scored_document_hits = {
@@ -1030,14 +1100,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // All documents in "Document" should be returned. The presence of
   // "nonexistentDocument" should have no effect.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
   EXPECT_THAT(page_result.results.at(0).document(), EqualsProto(document2));
@@ -1054,8 +1126,9 @@
                                 .SetScore(1)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document2 = DocumentBuilder()
@@ -1064,8 +1137,9 @@
                                 .SetScore(2)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   DocumentProto document3 = DocumentBuilder()
@@ -1074,8 +1148,9 @@
                                 .SetScore(3)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store_->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store_->Put(document_util::CreateDocumentWrapper(document3)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   DocumentProto document4 = DocumentBuilder()
@@ -1084,8 +1159,9 @@
                                 .SetScore(4)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             document_store_->Put(document4));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      document_store_->Put(document_util::CreateDocumentWrapper(document4)));
   DocumentId document_id4 = put_result4.new_document_id;
 
   DocumentProto document5 = DocumentBuilder()
@@ -1094,8 +1170,9 @@
                                 .SetScore(5)
                                 .SetCreationTimestampMs(1000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result5,
-                             document_store_->Put(document5));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result5,
+      document_store_->Put(document_util::CreateDocumentWrapper(document5)));
   DocumentId document_id5 = put_result5.new_document_id;
 
   std::vector<ScoredDocumentHit> scored_document_hits = {
@@ -1122,13 +1199,13 @@
   entry = result_grouping->add_entry_groupings();
   entry->set_namespace_("namespace2");
 
-  // Get corpus ids.
-  ICING_ASSERT_OK_AND_ASSIGN(
-      CorpusId corpus_id1, document_store_->GetResultGroupingEntryId(
-                               result_grouping_type, "namespace1", "Document"));
-  ICING_ASSERT_OK_AND_ASSIGN(
-      CorpusId corpus_id2, document_store_->GetResultGroupingEntryId(
-                               result_grouping_type, "namespace2", "Document"));
+  // Get result grouping entry ids.
+  ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
+      int32_t entry_id1, document_store_->GetResultGroupingEntryId(
+                             result_grouping_type, "namespace1", "Document"));
+  ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
+      int32_t entry_id2, document_store_->GetResultGroupingEntryId(
+                             result_grouping_type, "namespace2", "Document"));
 
   // Creates a ResultState with 5 ScoredDocumentHits.
   ResultStateV2 result_state(
@@ -1141,22 +1218,24 @@
     absl_ports::shared_lock l(&result_state.mutex);
 
     ASSERT_THAT(result_state.entry_id_group_id_map(),
-                UnorderedElementsAre(Pair(corpus_id1, 0), Pair(corpus_id2, 1)));
+                UnorderedElementsAre(Pair(entry_id1, 0), Pair(entry_id2, 1)));
     ASSERT_THAT(result_state.group_result_limits, ElementsAre(3, 1));
   }
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // document5, document4, document1 belong to namespace2 (with max_results =
   // 1).
-  // docuemnt3, document2 belong to namespace 1 (with max_results = 3).
+  // document3, document2 belong to namespace 1 (with max_results = 3).
   // Since num_per_page is 2, we expect to get document5 and document3 in the
   // first page.
   auto [page_result1, has_more_results1] = result_retriever->RetrieveNextPage(
-      result_state, fake_clock_.GetSystemTimeMilliseconds());
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
   ASSERT_THAT(page_result1.results, SizeIs(2));
   ASSERT_THAT(page_result1.results.at(0).document(), EqualsProto(document5));
   ASSERT_THAT(page_result1.results.at(1).document(), EqualsProto(document3));
@@ -1179,9 +1258,9 @@
     // round, num_returned should still be 2, since document4 was "filtered out"
     // and should not be counted into num_returned.
     EXPECT_THAT(result_state.num_returned, Eq(2));
-    // corpus_id_group_id_map should be unchanged.
+    // entry_id_group_id_map should be unchanged.
     EXPECT_THAT(result_state.entry_id_group_id_map(),
-                UnorderedElementsAre(Pair(corpus_id1, 0), Pair(corpus_id2, 1)));
+                UnorderedElementsAre(Pair(entry_id1, 0), Pair(entry_id2, 1)));
     // GroupResultLimiter should decrement the # in group_result_limits.
     EXPECT_THAT(result_state.group_result_limits, ElementsAre(2, 0));
   }
@@ -1189,7 +1268,8 @@
   // Although there are document2 and document1 left, since namespace2 has
   // reached its max results, document1 should be excluded from the second page.
   auto [page_result2, has_more_results2] = result_retriever->RetrieveNextPage(
-      result_state, fake_clock_.GetSystemTimeMilliseconds());
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
   ASSERT_THAT(page_result2.results, SizeIs(1));
   ASSERT_THAT(page_result2.results.at(0).document(), EqualsProto(document2));
   ASSERT_FALSE(has_more_results2);
@@ -1203,9 +1283,9 @@
     // since document1 was "filtered out" and should not be counted into
     // num_returned.
     EXPECT_THAT(result_state.num_returned, Eq(3));
-    // corpus_id_group_id_map should be unchanged.
+    // entry_id_group_id_map should be unchanged.
     EXPECT_THAT(result_state.entry_id_group_id_map(),
-                UnorderedElementsAre(Pair(corpus_id1, 0), Pair(corpus_id2, 1)));
+                UnorderedElementsAre(Pair(entry_id1, 0), Pair(entry_id2, 1)));
     // GroupResultLimiter should decrement the # in group_result_limits.
     EXPECT_THAT(result_state.group_result_limits, ElementsAre(1, 0));
   }
diff --git a/icing/result/result-retriever-v2_projection_test.cc b/icing/result/result-retriever-v2_projection_test.cc
index dfe7bb7..40bf19d 100644
--- a/icing/result/result-retriever-v2_projection_test.cc
+++ b/icing/result/result-retriever-v2_projection_test.cc
@@ -27,6 +27,7 @@
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/index/embed/embedding-query-results.h"
 #include "icing/portable/equals-proto.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
@@ -55,6 +56,7 @@
 #include "icing/transform/normalizer-factory.h"
 #include "icing/transform/normalizer-options.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/document-util.h"
 #include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
@@ -200,14 +202,18 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, test_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, test_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
   }
 
@@ -282,8 +288,9 @@
           .AddStringProperty(
               "body", "Oh what a beautiful morning! Oh what a beautiful day!")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document_one));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_one)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document_two =
@@ -295,8 +302,9 @@
           .AddStringProperty("body",
                              "Count all the sheep and tell them 'Hello'.")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document_two));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_two)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // 2. Setup the scored results.
@@ -330,13 +338,15 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned results only contain the 'name' property.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
@@ -380,8 +390,9 @@
           .AddStringProperty(
               "body", "Oh what a beautiful morning! Oh what a beautiful day!")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document_one));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_one)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document_two =
@@ -400,8 +411,9 @@
           .AddStringProperty("body",
                              "Count all the sheep and tell them 'Hello'.")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document_two));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_two)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // 2. Setup the scored results.
@@ -434,14 +446,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned results only contain the 'sender.name'
   // property.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
@@ -495,8 +509,9 @@
           .AddStringProperty(
               "body", "Oh what a beautiful morning! Oh what a beautiful day!")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document_one));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_one)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document_two =
@@ -515,8 +530,9 @@
           .AddStringProperty("body",
                              "Count all the sheep and tell them 'Hello'.")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document_two));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_two)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // 2. Setup the scored results.
@@ -549,14 +565,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned results only contain the 'sender'
   // property and all of the subproperties of 'sender'.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
@@ -613,8 +631,9 @@
           .AddStringProperty(
               "body", "Oh what a beautiful morning! Oh what a beautiful day!")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document_one));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_one)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document_two =
@@ -633,8 +652,9 @@
           .AddStringProperty("body",
                              "Count all the sheep and tell them 'Hello'.")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document_two));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_two)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // 2. Setup the scored results.
@@ -668,14 +688,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned results only contain the 'sender.name' and
   // 'sender.address' properties.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
@@ -724,8 +746,9 @@
           .AddStringProperty(
               "body", "Oh what a beautiful morning! Oh what a beautiful day!")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document_one));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_one)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document_two =
@@ -737,8 +760,9 @@
           .AddStringProperty("body",
                              "Count all the sheep and tell them 'Hello'.")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document_two));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_two)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // 2. Setup the scored results.
@@ -770,13 +794,15 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned results contain *no* properties.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
@@ -808,8 +834,9 @@
           .AddStringProperty(
               "body", "Oh what a beautiful morning! Oh what a beautiful day!")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document_one));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_one)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document_two =
@@ -821,8 +848,9 @@
           .AddStringProperty("body",
                              "Count all the sheep and tell them 'Hello'.")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document_two));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_two)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // 2. Setup the scored results.
@@ -855,13 +883,15 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned results contain *no* properties.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
@@ -893,8 +923,9 @@
           .AddStringProperty(
               "body", "Oh what a beautiful morning! Oh what a beautiful day!")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document_one));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_one)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document_two =
@@ -906,8 +937,9 @@
           .AddStringProperty("body",
                              "Count all the sheep and tell them 'Hello'.")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document_two));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_two)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // 2. Setup the scored results.
@@ -941,13 +973,15 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned results only contain the 'name' property.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
@@ -983,8 +1017,9 @@
           .AddStringProperty(
               "body", "Oh what a beautiful morning! Oh what a beautiful day!")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document_one));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_one)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document_two =
@@ -995,8 +1030,9 @@
           .AddStringProperty("name", "Joe Fox")
           .AddStringProperty("emailAddress", "ny152@aol.com")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document_two));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_two)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // 2. Setup the scored results.
@@ -1029,14 +1065,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned Email results only contain the 'name'
   // property and the returned Person results have all of their properties.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
@@ -1073,8 +1111,9 @@
           .AddStringProperty(
               "body", "Oh what a beautiful morning! Oh what a beautiful day!")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document_one));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_one)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document_two =
@@ -1085,8 +1124,9 @@
           .AddStringProperty("name", "Joe Fox")
           .AddStringProperty("emailAddress", "ny152@aol.com")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document_two));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_two)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // 2. Setup the scored results.
@@ -1121,14 +1161,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned Email results only contain the 'name'
   // property and the returned Person results only contain the 'name' property.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
@@ -1165,8 +1207,9 @@
           .AddStringProperty(
               "body", "Oh what a beautiful morning! Oh what a beautiful day!")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document_one));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_one)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document_two =
@@ -1177,8 +1220,9 @@
           .AddStringProperty("name", "Joe Fox")
           .AddStringProperty("emailAddress", "ny152@aol.com")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document_two));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_two)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // 2. Setup the scored results.
@@ -1217,14 +1261,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned Email results only contain the 'body'
   // property and the returned Person results  only contain the 'name' property.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
@@ -1270,8 +1316,9 @@
                   .AddStringProperty("emailAddress", "mr.body123@gmail.com")
                   .Build())
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document_one));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_one)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document_two =
@@ -1282,8 +1329,9 @@
           .AddStringProperty("name", "Joe Fox")
           .AddStringProperty("emailAddress", "ny152@aol.com")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document_two));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_two)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // 2. Setup the scored results.
@@ -1322,14 +1370,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned Email results only contain the 'sender.name'
   // property and the returned Person results only contain the 'name' property.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
@@ -1379,8 +1429,9 @@
                   .AddStringProperty("emailAddress", "mr.body123@gmail.com")
                   .Build())
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document_one));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_one)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document_two =
@@ -1391,8 +1442,9 @@
           .AddStringProperty("name", "Joe Fox")
           .AddStringProperty("emailAddress", "ny152@aol.com")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document_two));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_two)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // 2. Setup the scored results.
@@ -1431,14 +1483,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned Email results only contain the 'sender.name'
   // property and the returned Person results contain no properties.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
@@ -1476,8 +1530,10 @@
           .AddStringProperty("name", "Joe Fox")
           .AddStringProperty("emailAddress", "ny152@aol.com")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(person_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(person_document)));
   DocumentId person_document_id = put_result1.new_document_id;
 
   // 2. Add two Email documents
@@ -1491,8 +1547,9 @@
               "body", "Oh what a beautiful morning! Oh what a beautiful day!")
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(put_result1,
-                             document_store_->Put(email_document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result1, document_store_->Put(
+                       document_util::CreateDocumentWrapper(email_document1)));
   DocumentId email_document_id1 = put_result1.new_document_id;
 
   DocumentProto email_document2 =
@@ -1504,8 +1561,10 @@
           .AddStringProperty("body",
                              "Count all the sheep and tell them 'Hello'.")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(email_document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(email_document2)));
   DocumentId email_document_id2 = put_result2.new_document_id;
 
   // 3. Setup the joined scored results.
@@ -1569,15 +1628,17 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 7. Verify that the returned results:
   //    - Person docs only contain the "name" property.
   //    - Email docs only contain the "body" property.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(1));
 
@@ -1628,8 +1689,9 @@
           .AddStringProperty("name", "Joe Fox")
           .AddStringProperty("emailAddress", "ny152@aol.com")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document_one));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_one)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document_two =
@@ -1640,8 +1702,9 @@
           .AddStringProperty("name", "Joe Artist")
           .AddStringProperty("emailAddress", "artist@aol.com")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document_two));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_two)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // 2. Setup the scored results.
@@ -1674,14 +1737,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned Person and Artist results only contain the
   // 'name' property.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
@@ -1716,8 +1781,9 @@
           .AddStringProperty("name", "Joe Fox")
           .AddStringProperty("emailAddress", "ny152@aol.com")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document_one));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_one)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document_two =
@@ -1728,8 +1794,9 @@
           .AddStringProperty("name", "Joe Musician")
           .AddStringProperty("emailAddress", "Musician@aol.com")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document_two));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_two)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // 2. Setup the scored results.
@@ -1762,14 +1829,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned Person and Musician results only contain the
   // 'name' property.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
@@ -1804,8 +1873,9 @@
                                .SetSchema("Artist")
                                .AddStringProperty("name", "Joe Artist")
                                .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocumentId document_id = put_result.new_document_id;
 
   // 2. Setup the scored results.
@@ -1838,14 +1908,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned person document does not contain any property,
   // since 'emailAddress' is missing.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(1));
   DocumentProto projected_document = DocumentBuilder()
@@ -1867,8 +1939,9 @@
           .AddStringProperty("name", "Joe Fox")
           .AddStringProperty("emailAddress", "ny152@aol.com")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document_one));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_one)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   DocumentProto document_two =
@@ -1879,8 +1952,9 @@
           .AddStringProperty("name", "Joe Artist")
           .AddStringProperty("emailAddress", "artist@aol.com")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document_two));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(document_util::CreateDocumentWrapper(document_two)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // 2. Setup the scored results.
@@ -1918,15 +1992,17 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned Person results only contain the 'name'
   // property and the returned Artist results contain both the 'name' and
   // 'emailAddress' properties.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
@@ -1963,8 +2039,9 @@
                                .AddStringProperty("phoneNumber", "12345")
                                .AddStringProperty("phoneModel", "pixel")
                                .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocumentId document_id = put_result.new_document_id;
 
   // 2. Setup the scored results.
@@ -2003,14 +2080,16 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // 5. Verify that the returned document only contains the 'name' and the
   // 'phoneNumber' property.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(1));
 
diff --git a/icing/result/result-retriever-v2_snippet_test.cc b/icing/result/result-retriever-v2_snippet_test.cc
index 09b691e..b2bf82c 100644
--- a/icing/result/result-retriever-v2_snippet_test.cc
+++ b/icing/result/result-retriever-v2_snippet_test.cc
@@ -29,6 +29,7 @@
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/index/embed/embedding-query-results.h"
 #include "icing/portable/equals-proto.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
@@ -58,6 +59,7 @@
 #include "icing/transform/normalizer-factory.h"
 #include "icing/transform/normalizer-options.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/document-util.h"
 #include "icing/util/icu-data-file-helper.h"
 #include "icing/util/snippet-helpers.h"
 #include "unicode/uloc.h"
@@ -147,14 +149,18 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, test_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, test_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
   }
 
@@ -271,31 +277,35 @@
 EmbeddingQueryResults CreateEmailEmbeddingQueryResults(int num_documents) {
   SectionId embedding1_section_id = 1;
   SectionId embedding2_section_id = 2;
-  EmbeddingQueryResults embedding_query_results;
+  EmbeddingQueryResults embedding_query_results(/*num_query_vectors=*/2);
 
   for (int doc_id = 0; doc_id < num_documents; ++doc_id) {
     EmbeddingMatchInfos& info_model1 =
-        embedding_query_results
-            .result_infos[/*query_index=*/0][EMBEDDING_METRIC_DOT_PRODUCT]
-                         [doc_id];
-    info_model1.AppendScore(1.1 + doc_id);
-    info_model1.AppendScore(2.2 + doc_id);
+        GetOrCreateEmbeddingMatchInfosForDocument(
+            embedding_query_results, /*query_index=*/0,
+            EMBEDDING_METRIC_DOT_PRODUCT, doc_id);
+    info_model1.AppendScore(*embedding_query_results.global_scores,
+                            1.1 + doc_id);
     // {2, 3, 4 + doc_id}, position 2
     info_model1.AppendSectionInfo(
-        embedding1_section_id,
+        *embedding_query_results.global_section_infos, embedding1_section_id,
         /*position_in_section_for_dimension_and_signature=*/1);
+    info_model1.AppendScore(*embedding_query_results.global_scores,
+                            2.2 + doc_id);
     // {-1, -2, -6 + doc_id}, position 1
     info_model1.AppendSectionInfo(
-        embedding2_section_id,
+        *embedding_query_results.global_section_infos, embedding2_section_id,
         /*position_in_section_for_dimension_and_signature=*/0);
 
     EmbeddingMatchInfos& info_model2 =
-        embedding_query_results
-            .result_infos[/*query_index=*/1][EMBEDDING_METRIC_COSINE][doc_id];
-    info_model2.AppendScore(3.3 + doc_id);
+        GetOrCreateEmbeddingMatchInfosForDocument(
+            embedding_query_results, /*query_index=*/1, EMBEDDING_METRIC_COSINE,
+            doc_id);
+    info_model2.AppendScore(*embedding_query_results.global_scores,
+                            3.3 + doc_id);
     // {1, 2, 3, 4 + doc_id}, position 1
     info_model2.AppendSectionInfo(
-        embedding1_section_id,
+        *embedding_query_results.global_section_infos, embedding1_section_id,
         /*position_in_section_for_dimension_and_signature=*/0);
   }
   return embedding_query_results;
@@ -305,15 +315,18 @@
        DefaultSnippetSpecShouldDisableSnippeting) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(CreateEmailDocument(/*id=*/1)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(CreateEmailDocument(/*id=*/2)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
-      document_store_->Put(CreateEmailDocument(/*id=*/3)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/3))));
   DocumentId document_id3 = put_result3.new_document_id;
 
   std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "subject"),
@@ -326,7 +339,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   ResultSpecProto result_spec = CreateResultSpec(/*num_per_page=*/3);
 
@@ -345,8 +359,10 @@
       /*child_adjustment_info=*/nullptr, result_spec, *document_store_);
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(3));
   EXPECT_THAT(page_result.results.at(0).snippet(),
@@ -361,15 +377,18 @@
 TEST_F(ResultRetrieverV2SnippetTest, SimpleSnippeted) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(CreateEmailDocument(/*id=*/1)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(CreateEmailDocument(/*id=*/2)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
-      document_store_->Put(CreateEmailDocument(/*id=*/3)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/3))));
   DocumentId document_id3 = put_result3.new_document_id;
 
   std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "subject"),
@@ -382,7 +401,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Create ResultSpec with custom snippet spec.
   ResultSpecProto result_spec = CreateResultSpec(/*num_per_page=*/3);
@@ -404,8 +424,10 @@
 
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(3));
   EXPECT_THAT(page_result.num_results_with_snippets, Eq(3));
@@ -476,15 +498,18 @@
 TEST_F(ResultRetrieverV2SnippetTest, OnlyOneDocumentSnippeted) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(CreateEmailDocument(/*id=*/1)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(CreateEmailDocument(/*id=*/2)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
-      document_store_->Put(CreateEmailDocument(/*id=*/3)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/3))));
   DocumentId document_id3 = put_result3.new_document_id;
 
   std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "subject"),
@@ -497,7 +522,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Create ResultSpec with custom snippet spec.
   ResultSpecProto::SnippetSpecProto snippet_spec = CreateSnippetSpec();
@@ -521,8 +547,10 @@
 
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(3));
   EXPECT_THAT(page_result.num_results_with_snippets, Eq(1));
@@ -560,15 +588,18 @@
 TEST_F(ResultRetrieverV2SnippetTest, SnippetWithGetEmbeddingMatchInfo) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(CreateEmailDocument(/*id=*/1)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(CreateEmailDocument(/*id=*/2)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
-      document_store_->Put(CreateEmailDocument(/*id=*/3)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/3))));
   DocumentId document_id3 = put_result3.new_document_id;
 
   std::vector<SectionId> hit_section_ids = {
@@ -582,7 +613,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Create ResultSpec with custom snippet spec.
   ResultSpecProto::SnippetSpecProto snippet_spec = CreateSnippetSpec();
@@ -613,8 +645,10 @@
 
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(3));
   EXPECT_THAT(page_result.num_results_with_snippets, Eq(3));
@@ -738,15 +772,18 @@
 TEST_F(ResultRetrieverV2SnippetTest, SnippetWithGetEmbeddingMatchInfoDisabled) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(CreateEmailDocument(/*id=*/1)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(CreateEmailDocument(/*id=*/2)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
-      document_store_->Put(CreateEmailDocument(/*id=*/3)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/3))));
   DocumentId document_id3 = put_result3.new_document_id;
 
   std::vector<SectionId> hit_section_ids = {
@@ -760,7 +797,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Create ResultSpec with custom snippet spec.
   ResultSpecProto::SnippetSpecProto snippet_spec = CreateSnippetSpec();
@@ -791,8 +829,10 @@
 
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(3));
   EXPECT_THAT(page_result.num_results_with_snippets, Eq(3));
@@ -863,15 +903,18 @@
 TEST_F(ResultRetrieverV2SnippetTest, ShouldSnippetSomeResults) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(CreateEmailDocument(/*id=*/1)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(CreateEmailDocument(/*id=*/2)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
-      document_store_->Put(CreateEmailDocument(/*id=*/3)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/3))));
   DocumentId document_id3 = put_result3.new_document_id;
 
   std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "subject"),
@@ -884,7 +927,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Create ResultSpec with custom snippet spec.
   ResultSpecProto::SnippetSpecProto snippet_spec = CreateSnippetSpec();
@@ -914,8 +958,10 @@
 
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(3));
   EXPECT_THAT(page_result.results.at(0).snippet().entries(), Not(IsEmpty()));
@@ -927,15 +973,18 @@
 TEST_F(ResultRetrieverV2SnippetTest, ShouldNotSnippetAnyResults) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(CreateEmailDocument(/*id=*/1)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(CreateEmailDocument(/*id=*/2)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
-      document_store_->Put(CreateEmailDocument(/*id=*/3)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/3))));
   DocumentId document_id3 = put_result3.new_document_id;
 
   std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "subject"),
@@ -948,7 +997,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Create ResultSpec with custom snippet spec.
   ResultSpecProto::SnippetSpecProto snippet_spec = CreateSnippetSpec();
@@ -979,8 +1029,10 @@
   // We can't return any snippets for this page.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(3));
   EXPECT_THAT(page_result.results.at(0).snippet().entries(), IsEmpty());
@@ -993,15 +1045,18 @@
        ShouldNotSnippetAnyResultsForNonPositiveNumMatchesPerProperty) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(CreateEmailDocument(/*id=*/1)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(CreateEmailDocument(/*id=*/2)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
-      document_store_->Put(CreateEmailDocument(/*id=*/3)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/3))));
   DocumentId document_id3 = put_result3.new_document_id;
 
   std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "subject"),
@@ -1014,7 +1069,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Create ResultSpec with custom snippet spec.
   ResultSpecProto::SnippetSpecProto snippet_spec = CreateSnippetSpec();
@@ -1047,8 +1103,10 @@
   // We can't return any snippets for this page even though num_to_snippet > 0.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(3));
   EXPECT_THAT(page_result.results.at(0).snippet().entries(), IsEmpty());
@@ -1060,28 +1118,34 @@
 TEST_F(ResultRetrieverV2SnippetTest, JoinSnippeted) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult person_put_result1,
-      document_store_->Put(CreatePersonDocument(/*id=*/1)));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          CreatePersonDocument(/*id=*/1))));
   DocumentId person_document_id1 = person_put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult person_put_result2,
-      document_store_->Put(CreatePersonDocument(/*id=*/2)));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          CreatePersonDocument(/*id=*/2))));
   DocumentId person_document_id2 = person_put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult person_put_result3,
-      document_store_->Put(CreatePersonDocument(/*id=*/3)));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          CreatePersonDocument(/*id=*/3))));
   DocumentId person_document_id3 = person_put_result3.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_put_result1,
-      document_store_->Put(CreateEmailDocument(/*id=*/1)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/1))));
   DocumentId email_document_id1 = email_put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_put_result2,
-      document_store_->Put(CreateEmailDocument(/*id=*/2)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/2))));
   DocumentId email_document_id2 = email_put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_put_result3,
-      document_store_->Put(CreateEmailDocument(/*id=*/3)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/3))));
   DocumentId email_document_id3 = email_put_result3.new_document_id;
 
   std::vector<SectionId> person_hit_section_ids = {
@@ -1124,7 +1188,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Create parent ResultSpec with custom snippet spec.
   ResultSpecProto parent_result_spec = CreateResultSpec(/*num_per_page=*/3);
@@ -1163,8 +1228,10 @@
 
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(3));
   EXPECT_THAT(page_result.num_results_with_snippets, Eq(3));
@@ -1304,24 +1371,29 @@
 TEST_F(ResultRetrieverV2SnippetTest, ShouldSnippetAllJoinedResults) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult person_put_result1,
-      document_store_->Put(CreatePersonDocument(/*id=*/1)));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          CreatePersonDocument(/*id=*/1))));
   DocumentId person_document_id1 = person_put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult person_put_result2,
-      document_store_->Put(CreatePersonDocument(/*id=*/2)));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          CreatePersonDocument(/*id=*/2))));
   DocumentId person_document_id2 = person_put_result2.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_put_result1,
-      document_store_->Put(CreateEmailDocument(/*id=*/1)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/1))));
   DocumentId email_document_id1 = email_put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_put_result2,
-      document_store_->Put(CreateEmailDocument(/*id=*/2)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/2))));
   DocumentId email_document_id2 = email_put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_put_result3,
-      document_store_->Put(CreateEmailDocument(/*id=*/3)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/3))));
   DocumentId email_document_id3 = email_put_result3.new_document_id;
 
   std::vector<SectionId> person_hit_section_ids = {
@@ -1359,7 +1431,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Create parent ResultSpec with custom snippet spec.
   ResultSpecProto::SnippetSpecProto parent_snippet_spec = CreateSnippetSpec();
@@ -1403,8 +1476,10 @@
   // should be snippeted.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
@@ -1432,24 +1507,29 @@
 TEST_F(ResultRetrieverV2SnippetTest, ShouldSnippetSomeJoinedResults) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult person_put_result1,
-      document_store_->Put(CreatePersonDocument(/*id=*/1)));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          CreatePersonDocument(/*id=*/1))));
   DocumentId person_document_id1 = person_put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult person_put_result2,
-      document_store_->Put(CreatePersonDocument(/*id=*/2)));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          CreatePersonDocument(/*id=*/2))));
   DocumentId person_document_id2 = person_put_result2.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_put_result1,
-      document_store_->Put(CreateEmailDocument(/*id=*/1)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/1))));
   DocumentId email_document_id1 = email_put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_put_result2,
-      document_store_->Put(CreateEmailDocument(/*id=*/2)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/2))));
   DocumentId email_document_id2 = email_put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_put_result3,
-      document_store_->Put(CreateEmailDocument(/*id=*/3)));
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(CreateEmailDocument(/*id=*/3))));
   DocumentId email_document_id3 = email_put_result3.new_document_id;
 
   std::vector<SectionId> person_hit_section_ids = {
@@ -1487,7 +1567,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Create parent ResultSpec with custom snippet spec.
   ResultSpecProto::SnippetSpecProto parent_snippet_spec = CreateSnippetSpec();
@@ -1531,8 +1612,10 @@
   // snippeted.
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result.results, SizeIs(2));
 
diff --git a/icing/result/result-retriever-v2_test.cc b/icing/result/result-retriever-v2_test.cc
index 49899b1..6c3c727 100644
--- a/icing/result/result-retriever-v2_test.cc
+++ b/icing/result/result-retriever-v2_test.cc
@@ -19,6 +19,7 @@
 #include <cstdint>
 #include <limits>
 #include <memory>
+#include <optional>
 #include <string>
 #include <unordered_map>
 #include <utility>
@@ -35,6 +36,7 @@
 #include "icing/file/mock-filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/portable/equals-proto.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/document_wrapper.pb.h"
@@ -53,7 +55,6 @@
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
 #include "icing/testing/test-data.h"
-#include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 #include "icing/tokenization/language-segmenter-factory.h"
 #include "icing/tokenization/language-segmenter.h"
@@ -61,6 +62,7 @@
 #include "icing/transform/normalizer-options.h"
 #include "icing/transform/normalizer.h"
 #include "icing/util/clock.h"
+#include "icing/util/document-util.h"
 #include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
@@ -80,28 +82,30 @@
 using ::testing::SizeIs;
 using EntryIdMap = std::unordered_map<int32_t, int>;
 
-// Mock the behavior of GroupResultLimiter::ShouldBeRemoved.
+// Mock the behavior of GroupResultLimiter::GetGroupResultLimitsIndex: get
+// -1 to avoid excluding documents.
 class MockGroupResultLimiter : public GroupResultLimiterV2 {
  public:
   MockGroupResultLimiter() : GroupResultLimiterV2() {
-    ON_CALL(*this, ShouldBeRemoved).WillByDefault(Return(false));
+    ON_CALL(*this, GetGroupResultLimitsIndex).WillByDefault(Return(-1));
   }
 
-  MOCK_METHOD(bool, ShouldBeRemoved,
+  MOCK_METHOD(std::optional<int>, GetGroupResultLimitsIndex,
               (const ScoredDocumentHit&, const EntryIdMap&,
-               const DocumentStore&, std::vector<int>&,
-               ResultSpecProto::ResultGroupingType, int64_t),
+               const DocumentStore&, ResultSpecProto::ResultGroupingType,
+               int64_t),
               (const, override));
 };
 
-class ResultRetrieverV2Test : public ::testing::Test {
+class ResultRetrieverV2Test : public ::testing::TestWithParam<FeatureFlags> {
  protected:
   ResultRetrieverV2Test() : test_dir_(GetTestTempDir() + "/icing") {
     filesystem_.CreateDirectoryRecursively(test_dir_.c_str());
   }
 
   void SetUp() override {
-    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+    feature_flags_ = std::make_unique<FeatureFlags>(GetParam());
+
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
@@ -231,13 +235,17 @@
       /*force_recovery_and_revalidate_documents=*/false,
       /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
       PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
+      PortableFileBackedProtoLog<
+          DocumentWrapper>::kDefaultCompressionThresholdBytes,
+      protobuf_ports::kDefaultMemLevel,
       /*initialize_stats=*/nullptr);
 }
 
-TEST_F(ResultRetrieverV2Test, CreationWithNullPointerShouldFail) {
+TEST_P(ResultRetrieverV2Test, CreationWithNullPointerShouldFail) {
   EXPECT_THAT(
-      ResultRetrieverV2::Create(/*doc_store=*/nullptr, schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()),
+      ResultRetrieverV2::Create(
+          /*doc_store=*/nullptr, schema_store_.get(), language_segmenter_.get(),
+          normalizer_.get(), feature_flags_.get()),
       StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -249,19 +257,32 @@
 
   EXPECT_THAT(
       ResultRetrieverV2::Create(doc_store.get(), /*schema_store=*/nullptr,
-                                language_segmenter_.get(), normalizer_.get()),
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()),
+      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(
+      ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(),
+                                /*language_segmenter=*/nullptr,
+                                normalizer_.get(), feature_flags_.get()),
+      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(
+      ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(),
+                                language_segmenter_.get(),
+                                /*normalizer=*/nullptr, feature_flags_.get()),
+      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(
+      ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(),
+                                language_segmenter_.get(), normalizer_.get(),
+                                /*feature_flags=*/nullptr),
       StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
   EXPECT_THAT(ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(),
-                                        /*language_segmenter=*/nullptr,
-                                        normalizer_.get()),
-              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
-  EXPECT_THAT(ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(),
                                         language_segmenter_.get(),
-                                        /*normalizer=*/nullptr),
+                                        normalizer_.get(), feature_flags_.get(),
+                                        /*group_result_limiter=*/nullptr),
               StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 }
 
-TEST_F(ResultRetrieverV2Test, ShouldRetrieveSimpleResults) {
+TEST_P(ResultRetrieverV2Test, ShouldRetrieveSimpleResults) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
@@ -269,20 +290,30 @@
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(CreateDocument(/*id=*/1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(CreateDocument(/*id=*/2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             doc_store->Put(CreateDocument(/*id=*/3)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/3))));
   DocumentId document_id3 = put_result3.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             doc_store->Put(CreateDocument(/*id=*/4)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/4))));
   DocumentId document_id4 = put_result4.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result5,
-                             doc_store->Put(CreateDocument(/*id=*/5)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result5,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/5))));
   DocumentId document_id5 = put_result5.new_document_id;
 
   std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "name"),
@@ -297,7 +328,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   SearchResultProto::ResultProto result1;
   *result1.mutable_document() = CreateDocument(/*id=*/1);
@@ -325,7 +357,8 @@
 
   // First page, 2 results
   auto [page_result1, has_more_results1] = result_retriever->RetrieveNextPage(
-      result_state, fake_clock_.GetSystemTimeMilliseconds());
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
   EXPECT_THAT(page_result1.results,
               ElementsAre(EqualsProto(result1), EqualsProto(result2)));
   // num_results_with_snippets is 0 when there is no snippet.
@@ -337,7 +370,8 @@
 
   // Second page, 2 results
   auto [page_result2, has_more_results2] = result_retriever->RetrieveNextPage(
-      result_state, fake_clock_.GetSystemTimeMilliseconds());
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
   EXPECT_THAT(page_result2.results,
               ElementsAre(EqualsProto(result3), EqualsProto(result4)));
   // num_results_with_snippets is 0 when there is no snippet.
@@ -349,7 +383,8 @@
 
   // Third page, 1 result
   auto [page_result3, has_more_results3] = result_retriever->RetrieveNextPage(
-      result_state, fake_clock_.GetSystemTimeMilliseconds());
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
   EXPECT_THAT(page_result3.results, ElementsAre(EqualsProto(result5)));
   // num_results_with_snippets is 0 when there is no snippet.
   EXPECT_THAT(page_result3.num_results_with_snippets, Eq(0));
@@ -359,7 +394,7 @@
   EXPECT_FALSE(has_more_results3);
 }
 
-TEST_F(ResultRetrieverV2Test, ShouldIgnoreNonInternalErrors) {
+TEST_P(ResultRetrieverV2Test, ShouldIgnoreNonInternalErrors) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
@@ -367,11 +402,15 @@
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(CreateDocument(/*id=*/1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(CreateDocument(/*id=*/2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
 
   DocumentId invalid_document_id = -1;
@@ -386,6 +425,7 @@
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(),
                                 language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get(),
                                 std::make_unique<MockGroupResultLimiter>()));
 
   SearchResultProto::ResultProto result1;
@@ -405,8 +445,10 @@
       *doc_store);
   PageResult page_result1 =
       result_retriever
-          ->RetrieveNextPage(result_state1,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state1,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   EXPECT_THAT(page_result1.results,
               ElementsAre(EqualsProto(result1), EqualsProto(result2)));
@@ -426,14 +468,16 @@
       *doc_store);
   PageResult page_result2 =
       result_retriever
-          ->RetrieveNextPage(result_state2,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state2,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   EXPECT_THAT(page_result2.results,
               ElementsAre(EqualsProto(result1), EqualsProto(result2)));
 }
 
-TEST_F(ResultRetrieverV2Test,
+TEST_P(ResultRetrieverV2Test,
        ShouldLimitNumChildDocumentsByMaxJoinedChildPerParent) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
@@ -451,8 +495,9 @@
           .AddStringProperty("name", "Joe Fox")
           .AddStringProperty("emailAddress", "ny152@aol.com")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(person_document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(document_util::CreateDocumentWrapper(person_document1)));
   DocumentId person_document_id1 = put_result1.new_document_id;
 
   DocumentProto person_document2 =
@@ -463,8 +508,9 @@
           .AddStringProperty("name", "Meg Ryan")
           .AddStringProperty("emailAddress", "shopgirl@aol.com")
           .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(person_document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(document_util::CreateDocumentWrapper(person_document2)));
   DocumentId person_document_id2 = put_result2.new_document_id;
 
   // 2. Add 4 Email documents
@@ -475,7 +521,9 @@
                                       .AddStringProperty("name", "Test 1")
                                       .AddStringProperty("body", "Test 1")
                                       .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(put_result1, doc_store->Put(email_document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result1,
+      doc_store->Put(document_util::CreateDocumentWrapper(email_document1)));
   DocumentId email_document_id1 = put_result1.new_document_id;
 
   DocumentProto email_document2 = DocumentBuilder()
@@ -485,7 +533,9 @@
                                       .AddStringProperty("name", "Test 2")
                                       .AddStringProperty("body", "Test 2")
                                       .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(put_result2, doc_store->Put(email_document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result2,
+      doc_store->Put(document_util::CreateDocumentWrapper(email_document2)));
   DocumentId email_document_id2 = put_result2.new_document_id;
 
   DocumentProto email_document3 = DocumentBuilder()
@@ -495,8 +545,9 @@
                                       .AddStringProperty("name", "Test 3")
                                       .AddStringProperty("body", "Test 3")
                                       .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             doc_store->Put(email_document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      doc_store->Put(document_util::CreateDocumentWrapper(email_document3)));
   DocumentId email_document_id3 = put_result3.new_document_id;
 
   DocumentProto email_document4 = DocumentBuilder()
@@ -506,8 +557,9 @@
                                       .AddStringProperty("name", "Test 4")
                                       .AddStringProperty("body", "Test 4")
                                       .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             doc_store->Put(email_document4));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      doc_store->Put(document_util::CreateDocumentWrapper(email_document4)));
   DocumentId email_document_id4 = put_result4.new_document_id;
 
   // 3. Setup the joined scored results.
@@ -555,7 +607,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
   ResultStateV2 result_state(
       std::make_unique<
           PriorityQueueScoredDocumentHitsRanker<JoinedScoredDocumentHit>>(
@@ -583,14 +636,15 @@
   child3->set_score(3);
 
   auto [page_result, has_more_results] = result_retriever->RetrieveNextPage(
-      result_state, fake_clock_.GetSystemTimeMilliseconds());
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
   EXPECT_THAT(page_result.results,
               ElementsAre(EqualsProto(result1), EqualsProto(result2)));
   // No more results.
   EXPECT_FALSE(has_more_results);
 }
 
-TEST_F(ResultRetrieverV2Test, ShouldIgnoreInternalErrors) {
+TEST_P(ResultRetrieverV2Test, ShouldIgnoreInternalErrors) {
   MockFilesystem mock_filesystem;
   EXPECT_CALL(mock_filesystem,
               PRead(A<int>(), A<void*>(), A<size_t>(), A<off_t>()))
@@ -604,11 +658,15 @@
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(CreateDocument(/*id=*/1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(CreateDocument(/*id=*/2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
 
   std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "name"),
@@ -622,6 +680,7 @@
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(),
                                 language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get(),
                                 std::make_unique<MockGroupResultLimiter>()));
 
   SearchResultProto::ResultProto result1;
@@ -638,15 +697,17 @@
       *doc_store);
   PageResult page_result =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   // We mocked mock_filesystem to return an internal error when retrieving doc2,
   // so doc2 should be skipped and doc1 should still be returned.
   EXPECT_THAT(page_result.results, ElementsAre(EqualsProto(result1)));
 }
 
-TEST_F(ResultRetrieverV2Test, ShouldUpdateResultState) {
+TEST_P(ResultRetrieverV2Test, ShouldUpdateResultState) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
@@ -654,20 +715,30 @@
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(CreateDocument(/*id=*/1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(CreateDocument(/*id=*/2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             doc_store->Put(CreateDocument(/*id=*/3)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/3))));
   DocumentId document_id3 = put_result3.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             doc_store->Put(CreateDocument(/*id=*/4)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/4))));
   DocumentId document_id4 = put_result4.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result5,
-                             doc_store->Put(CreateDocument(/*id=*/5)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result5,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/5))));
   DocumentId document_id5 = put_result5.new_document_id;
 
   std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "name"),
@@ -682,7 +753,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   ResultStateV2 result_state(
       std::make_unique<
@@ -696,8 +768,10 @@
   // First page, 2 results
   PageResult page_result1 =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result1.results, SizeIs(2));
   {
@@ -713,8 +787,10 @@
   // Second page, 2 results
   PageResult page_result2 =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result2.results, SizeIs(2));
   {
@@ -730,8 +806,10 @@
   // Third page, 1 result
   PageResult page_result3 =
       result_retriever
-          ->RetrieveNextPage(result_state,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              result_state,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result3.results, SizeIs(1));
   {
@@ -745,7 +823,7 @@
   }
 }
 
-TEST_F(ResultRetrieverV2Test, ShouldUpdateNumTotalHits) {
+TEST_P(ResultRetrieverV2Test, ShouldUpdateNumTotalHits) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
@@ -757,11 +835,15 @@
                                             GetSectionId("Email", "body")};
   SectionIdMask hit_section_id_mask = CreateSectionIdMask(hit_section_ids);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(CreateDocument(/*id=*/1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(CreateDocument(/*id=*/2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
   std::vector<ScoredDocumentHit> scored_document_hits1 = {
       {document_id1, hit_section_id_mask, /*score=*/0},
@@ -782,14 +864,20 @@
     ASSERT_THAT(num_total_hits_, Eq(2));
   }
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             doc_store->Put(CreateDocument(/*id=*/3)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/3))));
   DocumentId document_id3 = put_result3.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             doc_store->Put(CreateDocument(/*id=*/4)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/4))));
   DocumentId document_id4 = put_result4.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result5,
-                             doc_store->Put(CreateDocument(/*id=*/5)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result5,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/5))));
   DocumentId document_id5 = put_result5.new_document_id;
   std::vector<ScoredDocumentHit> scored_document_hits2 = {
       {document_id3, hit_section_id_mask, /*score=*/0},
@@ -814,14 +902,17 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   // Should get 1 doc in the first page of result_state1, and num_total_hits
   // should be decremented by 1.
   PageResult page_result1 =
       result_retriever
-          ->RetrieveNextPage(*result_state1,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              *result_state1,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result1.results, SizeIs(1));
   EXPECT_THAT(num_total_hits_, Eq(4));
@@ -830,8 +921,10 @@
   // should be decremented by 2.
   PageResult page_result2 =
       result_retriever
-          ->RetrieveNextPage(*result_state2,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              *result_state2,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result2.results, SizeIs(2));
   EXPECT_THAT(num_total_hits_, Eq(2));
@@ -841,8 +934,10 @@
   // by 1.
   PageResult page_result3 =
       result_retriever
-          ->RetrieveNextPage(*result_state2,
-                             fake_clock_.GetSystemTimeMilliseconds())
+          ->RetrieveNextPage(
+              *result_state2,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              fake_clock_.GetSystemTimeMilliseconds())
           .first;
   ASSERT_THAT(page_result3.results, SizeIs(1));
   EXPECT_THAT(num_total_hits_, Eq(1));
@@ -858,7 +953,7 @@
   EXPECT_THAT(num_total_hits_, Eq(0));
 }
 
-TEST_F(ResultRetrieverV2Test, ShouldLimitNumTotalBytesPerPage) {
+TEST_P(ResultRetrieverV2Test, ShouldLimitNumTotalBytesPerPage) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
@@ -866,11 +961,15 @@
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(CreateDocument(/*id=*/1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(CreateDocument(/*id=*/2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
 
   std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "name"),
@@ -882,7 +981,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   SearchResultProto::ResultProto result1;
   *result1.mutable_document() = CreateDocument(/*id=*/1);
@@ -906,20 +1006,22 @@
   // num_total_bytes_per_page_threshold and ResultRetriever should terminate
   // early even though # of results is still below num_per_page.
   auto [page_result1, has_more_results1] = result_retriever->RetrieveNextPage(
-      result_state, fake_clock_.GetSystemTimeMilliseconds());
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
   EXPECT_THAT(page_result1.results, ElementsAre(EqualsProto(result1)));
   // Has more results.
   EXPECT_TRUE(has_more_results1);
 
   // Second page, result2.
   auto [page_result2, has_more_results2] = result_retriever->RetrieveNextPage(
-      result_state, fake_clock_.GetSystemTimeMilliseconds());
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
   EXPECT_THAT(page_result2.results, ElementsAre(EqualsProto(result2)));
   // No more results.
   EXPECT_FALSE(has_more_results2);
 }
 
-TEST_F(ResultRetrieverV2Test,
+TEST_P(ResultRetrieverV2Test,
        ShouldReturnSingleLargeResultAboveNumTotalBytesPerPageThreshold) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
@@ -928,11 +1030,15 @@
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(CreateDocument(/*id=*/1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(CreateDocument(/*id=*/2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
 
   std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "name"),
@@ -944,7 +1050,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   SearchResultProto::ResultProto result1;
   *result1.mutable_document() = CreateDocument(/*id=*/1);
@@ -970,21 +1077,27 @@
   // First page. Should return single result1 even though its byte size exceeds
   // num_total_bytes_per_page_threshold.
   auto [page_result1, has_more_results1] = result_retriever->RetrieveNextPage(
-      result_state, fake_clock_.GetSystemTimeMilliseconds());
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
   EXPECT_THAT(page_result1.results, ElementsAre(EqualsProto(result1)));
   // Has more results.
   EXPECT_TRUE(has_more_results1);
 
   // Second page, result2.
   auto [page_result2, has_more_results2] = result_retriever->RetrieveNextPage(
-      result_state, fake_clock_.GetSystemTimeMilliseconds());
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
   EXPECT_THAT(page_result2.results, ElementsAre(EqualsProto(result2)));
   // No more results.
   EXPECT_FALSE(has_more_results2);
 }
 
-TEST_F(ResultRetrieverV2Test,
+TEST_P(ResultRetrieverV2Test,
        ShouldRetrieveNextResultWhenBelowNumTotalBytesPerPageThreshold) {
+  if (feature_flags_->enable_strict_page_byte_size_limit()) {
+    GTEST_SKIP() << "Test only applies to non-strict page byte size limit.";
+  }
+
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
@@ -992,11 +1105,15 @@
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(CreateDocument(/*id=*/1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(CreateDocument(/*id=*/2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
 
   std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "name"),
@@ -1008,7 +1125,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<ResultRetrieverV2> result_retriever,
       ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(),
-                                language_segmenter_.get(), normalizer_.get()));
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
 
   SearchResultProto::ResultProto result1;
   *result1.mutable_document() = CreateDocument(/*id=*/1);
@@ -1036,13 +1154,225 @@
   // the retrieval process and thus include result2 into this page, even though
   // finally total bytes of result1 + result2 exceed the threshold.
   auto [page_result, has_more_results] = result_retriever->RetrieveNextPage(
-      result_state, fake_clock_.GetSystemTimeMilliseconds());
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
   EXPECT_THAT(page_result.results,
               ElementsAre(EqualsProto(result1), EqualsProto(result2)));
   // No more results.
   EXPECT_FALSE(has_more_results);
 }
 
+TEST_P(ResultRetrieverV2Test,
+       ShouldNotIncludeNextResultIfExceedingNumTotalBytesPerPageThreshold) {
+  if (!feature_flags_->enable_strict_page_byte_size_limit()) {
+    GTEST_SKIP() << "Test only applies to strict page byte size limit.";
+  }
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
+                          schema_store_.get(), *feature_flags_));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/1))));
+  DocumentId document_id1 = put_result1.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/2))));
+  DocumentId document_id2 = put_result2.new_document_id;
+
+  std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "name"),
+                                            GetSectionId("Email", "body")};
+  SectionIdMask hit_section_id_mask = CreateSectionIdMask(hit_section_ids);
+  std::vector<ScoredDocumentHit> scored_document_hits = {
+      {document_id1, hit_section_id_mask, /*score=*/5},
+      {document_id2, hit_section_id_mask, /*score=*/0}};
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<ResultRetrieverV2> result_retriever,
+      ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(),
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
+
+  SearchResultProto::ResultProto result1;
+  *result1.mutable_document() = CreateDocument(/*id=*/1);
+  result1.set_score(5);
+  SearchResultProto::ResultProto result2;
+  *result2.mutable_document() = CreateDocument(/*id=*/2);
+  result2.set_score(0);
+
+  // result1.ByteSizeLong() < threshold < result1.ByteSizeLong() +
+  //                                      result2.ByteSizeLong().
+  int threshold = result1.ByteSizeLong() + 1;
+  ASSERT_THAT(result1.ByteSizeLong() + result2.ByteSizeLong(), Gt(threshold));
+
+  ResultSpecProto result_spec =
+      CreateResultSpec(/*num_per_page=*/2, ResultSpecProto::NAMESPACE);
+  result_spec.set_num_total_bytes_per_page_threshold(threshold);
+  ResultStateV2 result_state(
+      std::make_unique<
+          PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>(
+          std::move(scored_document_hits),
+          /*is_descending=*/true),
+      /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr,
+      result_spec, *doc_store);
+
+  // After retrieving result1, total bytes are still below the threshold and #
+  // of results is still below num_per_page, so ResultRetriever should continue
+  // the retrieval process. But when serializing result2, the byte size exceeds
+  // the threshold, so result2 should not be included.
+  auto [page_result1, has_more_results1] = result_retriever->RetrieveNextPage(
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(page_result1.results, ElementsAre(EqualsProto(result1)));
+  // More results.
+  EXPECT_TRUE(has_more_results1);
+
+  // Second page. Even though result2 was evaluated in the previous round but
+  // excluded, it should not be popped from the ranker and thus should be
+  // included in the second page.
+  auto [page_result2, has_more_results2] = result_retriever->RetrieveNextPage(
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(page_result2.results, ElementsAre(EqualsProto(result2)));
+  // No more results.
+  EXPECT_FALSE(has_more_results2);
+}
+
+TEST_P(ResultRetrieverV2Test,
+       ResultGroupingShouldDecrementOnlyWhenResultIsIncludedInThePage) {
+  if (!feature_flags_->enable_strict_page_byte_size_limit()) {
+    GTEST_SKIP() << "Test only applies to non-strict page byte size limit.";
+  }
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
+                          schema_store_.get(), *feature_flags_));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/1))));
+  DocumentId document_id1 = put_result1.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/2))));
+  DocumentId document_id2 = put_result2.new_document_id;
+
+  std::vector<SectionId> hit_section_ids = {GetSectionId("Email", "name"),
+                                            GetSectionId("Email", "body")};
+  SectionIdMask hit_section_id_mask = CreateSectionIdMask(hit_section_ids);
+  std::vector<ScoredDocumentHit> scored_document_hits = {
+      {document_id1, hit_section_id_mask, /*score=*/5},
+      {document_id2, hit_section_id_mask, /*score=*/0}};
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<ResultRetrieverV2> result_retriever,
+      ResultRetrieverV2::Create(doc_store.get(), schema_store_.get(),
+                                language_segmenter_.get(), normalizer_.get(),
+                                feature_flags_.get()));
+
+  SearchResultProto::ResultProto result1;
+  *result1.mutable_document() = CreateDocument(/*id=*/1);
+  result1.set_score(5);
+  SearchResultProto::ResultProto result2;
+  *result2.mutable_document() = CreateDocument(/*id=*/2);
+  result2.set_score(0);
+
+  // Create a ResultSpec that limits namespace "icing" to 10 results and the
+  // total bytes per page to result1.ByteSizeLong() + 1. This will force the
+  // result retriever to move result2 to the next page.
+  ResultSpecProto result_spec =
+      CreateResultSpec(/*num_per_page=*/10, ResultSpecProto::NAMESPACE);
+  result_spec.set_num_total_bytes_per_page_threshold(result1.ByteSizeLong() +
+                                                     1);
+
+  ResultSpecProto::ResultGrouping* result_grouping =
+      result_spec.add_result_groupings();
+  ResultSpecProto::ResultGrouping::Entry* entry =
+      result_grouping->add_entry_groupings();
+  result_grouping->set_max_results(2);
+  entry->set_namespace_("icing");
+
+  // Creates a ResultState with 2 ScoredDocumentHits.
+  ResultStateV2 result_state(
+      std::make_unique<
+          PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>(
+          std::move(scored_document_hits), /*is_descending=*/true),
+      /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr,
+      result_spec, *doc_store);
+
+  // First page: result1 should be returned.
+  auto [page_result1, has_more_results1] = result_retriever->RetrieveNextPage(
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(page_result1.results, ElementsAre(EqualsProto(result1)));
+  // Has more results.
+  EXPECT_TRUE(has_more_results1);
+
+  // Second page: result2 should be returned.
+  auto [page_result2, has_more_results2] = result_retriever->RetrieveNextPage(
+      result_state, /*max_results=*/std::numeric_limits<int32_t>::max(),
+      fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(page_result2.results, ElementsAre(EqualsProto(result2)));
+  // No more results.
+  EXPECT_FALSE(has_more_results2);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    ResultRetrieverV2Test, ResultRetrieverV2Test,
+    testing::Values(FeatureFlags(
+                        /*allow_circular_schema_definitions=*/true,
+                        /*enable_scorable_properties=*/true,
+                        /*enable_embedding_quantization=*/true,
+                        /*enable_repeated_field_joins=*/true,
+                        /*enable_embedding_backup_generation=*/true,
+                        /*enable_schema_database=*/true,
+                        /*release_backup_schema_file_if_overlay_present=*/true,
+                        /*enable_strict_page_byte_size_limit=*/false,
+                        /*enable_smaller_decompression_buffer_size=*/true,
+                        /*enable_eigen_embedding_scoring=*/true,
+                        /*enable_passing_filter_to_children=*/true,
+                        /*enable_proto_log_new_header_format=*/true,
+                        /*enable_embedding_iterator_v2=*/true,
+                        /*enable_reusable_decompression_buffer=*/true,
+                        /*enable_schema_type_id_optimization=*/true,
+                        /*enable_optimize_improvements=*/true,
+                        /*expired_document_purge_threshold_ms=*/0,
+                        /*enable_non_existent_qualified_id_join=*/true,
+                        /*enable_skip_set_schema_type_equality_check=*/true,
+                        /*enable_embed_query_optimization=*/true,
+                        /*enable_schema_definition_deduping=*/true),
+                    FeatureFlags(
+                        /*allow_circular_schema_definitions=*/true,
+                        /*enable_scorable_properties=*/true,
+                        /*enable_embedding_quantization=*/true,
+                        /*enable_repeated_field_joins=*/true,
+                        /*enable_embedding_backup_generation=*/true,
+                        /*enable_schema_database=*/true,
+                        /*release_backup_schema_file_if_overlay_present=*/true,
+                        /*enable_strict_page_byte_size_limit=*/true,
+                        /*enable_smaller_decompression_buffer_size=*/true,
+                        /*enable_eigen_embedding_scoring=*/true,
+                        /*enable_passing_filter_to_children=*/true,
+                        /*enable_proto_log_new_header_format=*/true,
+                        /*enable_embedding_iterator_v2=*/true,
+                        /*enable_reusable_decompression_buffer=*/true,
+                        /*enable_schema_type_id_optimization=*/true,
+                        /*enable_optimize_improvements=*/true,
+                        /*expired_document_purge_threshold_ms=*/0,
+                        /*enable_non_existent_qualified_id_join=*/true,
+                        /*enable_skip_set_schema_type_equality_check=*/true,
+                        /*enable_embed_query_optimization=*/true,
+                        /*enable_schema_definition_deduping=*/true)));
+
 }  // namespace
 
 }  // namespace lib
diff --git a/icing/result/result-state-manager.cc b/icing/result/result-state-manager.cc
index 382f7db..ebd0512 100644
--- a/icing/result/result-state-manager.cc
+++ b/icing/result/result-state-manager.cc
@@ -14,18 +14,24 @@
 
 #include "icing/result/result-state-manager.h"
 
+#include <cstdint>
+#include <limits>
 #include <memory>
 #include <queue>
 #include <utility>
 
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/absl_ports/canonical_errors.h"
+#include "icing/absl_ports/mutex.h"
+#include "icing/proto/logging.pb.h"
 #include "icing/result/page-result.h"
 #include "icing/result/result-adjustment-info.h"
 #include "icing/result/result-retriever-v2.h"
 #include "icing/result/result-state-v2.h"
 #include "icing/scoring/scored-document-hits-ranker.h"
+#include "icing/store/document-store.h"
 #include "icing/util/clock.h"
 #include "icing/util/logging.h"
-#include "icing/util/status-macros.h"
 
 namespace icing {
 namespace lib {
@@ -43,7 +49,8 @@
     std::unique_ptr<ResultAdjustmentInfo> parent_adjustment_info,
     std::unique_ptr<ResultAdjustmentInfo> child_adjustment_info,
     const ResultSpecProto& result_spec, const DocumentStore& document_store,
-    const ResultRetrieverV2& result_retriever, int64_t current_time_ms) {
+    const ResultRetrieverV2& result_retriever, int64_t current_time_ms,
+    QueryStatsProto* query_stats) {
   if (ranker == nullptr) {
     return absl_ports::InvalidArgumentError("Should not provide null ranker");
   }
@@ -56,8 +63,9 @@
 
   // Retrieve docs outside of ResultStateManager critical section.
   // Will enter ResultState critical section inside ResultRetriever.
-  auto [page_result, has_more_results] =
-      result_retriever.RetrieveNextPage(*result_state, current_time_ms);
+  auto [page_result, has_more_results] = result_retriever.RetrieveNextPage(
+      *result_state,
+      /*max_results=*/std::numeric_limits<int32_t>::max(), current_time_ms);
   if (!has_more_results) {
     // No more pages, won't store ResultState, returns directly
     return std::make_pair(kInvalidNextPageToken, std::move(page_result));
@@ -87,7 +95,7 @@
     InternalInvalidateExpiredResultStates(kDefaultResultStateTtlInMs,
                                           current_time_ms);
     // Remove states to make room for this new state.
-    RemoveStatesIfNeeded(num_hits_to_add);
+    RemoveStatesIfNeeded(num_hits_to_add, query_stats);
     // Generate a new unique token and add it into result_state_map_.
     next_page_token = Add(std::move(result_state), current_time_ms);
   }
@@ -107,7 +115,7 @@
 }
 
 libtextclassifier3::StatusOr<std::pair<uint64_t, PageResult>>
-ResultStateManager::GetNextPage(uint64_t next_page_token,
+ResultStateManager::GetNextPage(uint64_t next_page_token, int32_t max_results,
                                 const ResultRetrieverV2& result_retriever,
                                 int64_t current_time_ms) {
   std::shared_ptr<ResultStateV2> result_state = nullptr;
@@ -128,8 +136,8 @@
 
   // Retrieve docs outside of ResultStateManager critical section.
   // Will enter ResultState critical section inside ResultRetriever.
-  auto [page_result, has_more_results] =
-      result_retriever.RetrieveNextPage(*result_state, current_time_ms);
+  auto [page_result, has_more_results] = result_retriever.RetrieveNextPage(
+      *result_state, max_results, current_time_ms);
 
   if (!has_more_results) {
     {
@@ -144,6 +152,14 @@
   return std::make_pair(next_page_token, std::move(page_result));
 }
 
+int ResultStateManager::GetNumActiveResultStates(int64_t current_time_ms) {
+  absl_ports::unique_lock l(&mutex_);
+
+  InternalInvalidateExpiredResultStates(kDefaultResultStateTtlInMs,
+                                        current_time_ms);
+  return result_state_map_.size();
+}
+
 void ResultStateManager::InvalidateResultState(uint64_t next_page_token) {
   if (next_page_token == kInvalidNextPageToken) {
     return;
@@ -181,16 +197,17 @@
   return new_token;
 }
 
-void ResultStateManager::RemoveStatesIfNeeded(int num_hits_to_add) {
+void ResultStateManager::RemoveStatesIfNeeded(int num_hits_to_add,
+                                              QueryStatsProto* query_stats) {
   if (result_state_map_.empty() || token_queue_.empty()) {
     return;
   }
 
   // 1. Check if this new result_state would take up the entire result state
-  // manager budget.
+  //    manager budget.
   if (num_hits_to_add > max_total_hits_) {
     // This single result state will exceed our budget. Drop everything else to
-    // accomodate it.
+    // accommodate it.
     InternalInvalidateAllResultStates();
     return;
   }
@@ -204,18 +221,33 @@
   }
 
   // 3. If we're over budget, remove states from oldest to newest until we fit
-  // into our budget.
+  //    into our budget.
+  //
   // Note: num_total_hits_ may not be decremented immediately after invalidating
   // a result state, since other threads may still hold the shared pointer.
   // Thus, we have to check if token_queue_ is empty or not, since it is
   // possible that num_total_hits_ is non-zero and still greater than
   // max_total_hits_ when token_queue_ is empty. Still "eventually" it will be
   // decremented after the last thread releases the shared pointer.
+  int num_states_evicted = 0;
   while (!token_queue_.empty() && num_total_hits_ > max_total_hits_) {
+    ICING_LOG(WARNING) << "Evicting result state from token_queue_ due to "
+                          "budget limit. Current num_total_hits_: "
+                       << num_total_hits_;
+
     InternalInvalidateResultState(token_queue_.front().first);
+    ++num_states_evicted;
     token_queue_.pop();
   }
   invalidated_token_set_.clear();
+  if (num_states_evicted > 0) {
+    ICING_LOG(WARNING) << "Evicted " << num_states_evicted
+                       << " states. After eviction: " << num_total_hits_
+                       << " hits and " << token_queue_.size() << " states.";
+    if (query_stats != nullptr) {
+      query_stats->set_num_result_states_evicted(num_states_evicted);
+    }
+  }
 }
 
 void ResultStateManager::InternalInvalidateResultState(uint64_t token) {
diff --git a/icing/result/result-state-manager.h b/icing/result/result-state-manager.h
index a64ae2c..b370c59 100644
--- a/icing/result/result-state-manager.h
+++ b/icing/result/result-state-manager.h
@@ -16,21 +16,25 @@
 #define ICING_RESULT_RESULT_STATE_MANAGER_H_
 
 #include <atomic>
+#include <cstdint>
 #include <memory>
 #include <queue>
 #include <random>
 #include <unordered_map>
 #include <unordered_set>
+#include <utility>
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/mutex.h"
+#include "icing/absl_ports/thread_annotations.h"
+#include "icing/proto/logging.pb.h"
 #include "icing/proto/search.pb.h"
 #include "icing/result/page-result.h"
 #include "icing/result/result-adjustment-info.h"
 #include "icing/result/result-retriever-v2.h"
 #include "icing/result/result-state-v2.h"
 #include "icing/scoring/scored-document-hits-ranker.h"
-#include "icing/util/clock.h"
+#include "icing/store/document-store.h"
 
 namespace icing {
 namespace lib {
@@ -77,10 +81,11 @@
       std::unique_ptr<ResultAdjustmentInfo> parent_adjustment_info,
       std::unique_ptr<ResultAdjustmentInfo> child_adjustment_info,
       const ResultSpecProto& result_spec, const DocumentStore& document_store,
-      const ResultRetrieverV2& result_retriever, int64_t current_time_ms)
-      ICING_LOCKS_EXCLUDED(mutex_);
+      const ResultRetrieverV2& result_retriever, int64_t current_time_ms,
+      QueryStatsProto* query_stats = nullptr) ICING_LOCKS_EXCLUDED(mutex_);
 
-  // Retrieves and returns PageResult for the next page.
+  // Retrieves and returns PageResult for the next page, retrieving at most
+  // max_results entries from the page.
   // The returned results won't exist in ResultStateManager anymore. If the
   // query has no more pages after this retrieval, the input token will be
   // invalidated.
@@ -93,8 +98,15 @@
   //   A token and PageResult wrapped by std::pair on success
   //   NOT_FOUND if failed to find any more results
   libtextclassifier3::StatusOr<std::pair<uint64_t, PageResult>> GetNextPage(
-      uint64_t next_page_token, const ResultRetrieverV2& result_retriever,
-      int64_t current_time_ms) ICING_LOCKS_EXCLUDED(mutex_);
+      uint64_t next_page_token, int32_t max_results,
+      const ResultRetrieverV2& result_retriever, int64_t current_time_ms)
+      ICING_LOCKS_EXCLUDED(mutex_);
+
+  // Returns the number of active result states currently in ResultStateManager.
+  // Note that this will invalidate expired result states before counting the
+  // number.
+  int GetNumActiveResultStates(int64_t current_time_ms)
+      ICING_LOCKS_EXCLUDED(mutex_);
 
   // Invalidates the result state associated with the given next-page token.
   void InvalidateResultState(uint64_t next_page_token)
@@ -150,7 +162,7 @@
 
   // Helper method to remove old states to make room for incoming states with
   // size num_hits_to_add.
-  void RemoveStatesIfNeeded(int num_hits_to_add)
+  void RemoveStatesIfNeeded(int num_hits_to_add, QueryStatsProto* query_stats)
       ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
 
   // Helper method to remove a result state from result_state_map_, the token
diff --git a/icing/result/result-state-manager_test.cc b/icing/result/result-state-manager_test.cc
index 463005e..45cc0f8 100644
--- a/icing/result/result-state-manager_test.cc
+++ b/icing/result/result-state-manager_test.cc
@@ -32,6 +32,7 @@
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/index/embed/embedding-query-results.h"
 #include "icing/portable/equals-proto.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/query/query-terms.h"
 #include "icing/result/page-result.h"
@@ -54,6 +55,7 @@
 #include "icing/transform/normalizer-factory.h"
 #include "icing/transform/normalizer-options.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/document-util.h"
 #include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
@@ -129,20 +131,25 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult result,
-        DocumentStore::Create(&filesystem_, test_dir_, clock_.get(),
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, test_dir_, clock_.get(), schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(result.document_store);
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        result_retriever_, ResultRetrieverV2::Create(
-                               document_store_.get(), schema_store_.get(),
-                               language_segmenter_.get(), normalizer_.get()));
+        result_retriever_,
+        ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
+                                  language_segmenter_.get(), normalizer_.get(),
+                                  feature_flags_.get()));
   }
 
   void TearDown() override {
@@ -157,10 +164,15 @@
     document.set_uri(std::to_string(document_id));
     document.set_schema("Document");
     document.set_creation_timestamp_ms(1574365086666 + document_id);
-    document_store_->Put(document);
+
+    DocumentWrapper document_wrapper;
+    *document_wrapper.mutable_document() = std::move(document);
+
+    document_store_->Put(document_wrapper);
+
     return std::make_pair(
         ScoredDocumentHit(document_id, kSectionIdMaskNone, /*score=*/1),
-        std::move(document));
+        std::move(*document_wrapper.mutable_document()));
   }
 
   std::pair<std::vector<ScoredDocumentHit>, std::vector<DocumentProto>>
@@ -207,14 +219,20 @@
 };
 
 TEST_F(ResultStateManagerTest, ShouldCacheAndRetrieveFirstPageOnePage) {
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store().Put(CreateDocument(/*id=*/1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store().Put(CreateDocument(/*id=*/2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store().Put(CreateDocument(/*id=*/3)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/3))));
   DocumentId document_id3 = put_result3.new_document_id;
   std::vector<ScoredDocumentHit> scored_document_hits = {
       {document_id1, kSectionIdMaskNone, /*score=*/1},
@@ -249,20 +267,30 @@
 }
 
 TEST_F(ResultStateManagerTest, ShouldCacheAndRetrieveFirstPageMultiplePages) {
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store().Put(CreateDocument(/*id=*/1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store().Put(CreateDocument(/*id=*/2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store().Put(CreateDocument(/*id=*/3)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/3))));
   DocumentId document_id3 = put_result3.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             document_store().Put(CreateDocument(/*id=*/4)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/4))));
   DocumentId document_id4 = put_result4.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result5,
-                             document_store().Put(CreateDocument(/*id=*/5)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result5,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/5))));
   DocumentId document_id5 = put_result5.new_document_id;
   std::vector<ScoredDocumentHit> scored_document_hits = {
       {document_id1, kSectionIdMaskNone, /*score=*/1},
@@ -298,8 +326,9 @@
   // Second page, 2 results
   ICING_ASSERT_OK_AND_ASSIGN(
       PageResultInfo page_result_info2,
-      result_state_manager.GetNextPage(next_page_token, result_retriever(),
-                                       clock()->GetSystemTimeMilliseconds()));
+      result_state_manager.GetNextPage(
+          next_page_token, /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   EXPECT_THAT(page_result_info2.first, Eq(next_page_token));
   ASSERT_THAT(page_result_info2.second.results, SizeIs(2));
   EXPECT_THAT(page_result_info2.second.results.at(0).document(),
@@ -310,8 +339,9 @@
   // Third page, 1 result
   ICING_ASSERT_OK_AND_ASSIGN(
       PageResultInfo page_result_info3,
-      result_state_manager.GetNextPage(next_page_token, result_retriever(),
-                                       clock()->GetSystemTimeMilliseconds()));
+      result_state_manager.GetNextPage(
+          next_page_token, /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   EXPECT_THAT(page_result_info3.first, Eq(kInvalidNextPageToken));
   ASSERT_THAT(page_result_info3.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info3.second.results.at(0).document(),
@@ -319,8 +349,9 @@
 
   // No results
   EXPECT_THAT(
-      result_state_manager.GetNextPage(next_page_token, result_retriever(),
-                                       clock()->GetSystemTimeMilliseconds()),
+      result_state_manager.GetNextPage(
+          next_page_token, /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()),
       StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 }
 
@@ -357,11 +388,15 @@
 }
 
 TEST_F(ResultStateManagerTest, ShouldAllowEmptyFirstPage) {
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store().Put(CreateDocument(/*id=*/1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store().Put(CreateDocument(/*id=*/2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
   std::vector<ScoredDocumentHit> scored_document_hits = {
       {document_id1, kSectionIdMaskNone, /*score=*/1},
@@ -396,17 +431,25 @@
 }
 
 TEST_F(ResultStateManagerTest, ShouldAllowEmptyLastPage) {
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store().Put(CreateDocument(/*id=*/1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store().Put(CreateDocument(/*id=*/2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store().Put(CreateDocument(/*id=*/3)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/3))));
   DocumentId document_id3 = put_result3.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             document_store().Put(CreateDocument(/*id=*/4)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/4))));
   DocumentId document_id4 = put_result4.new_document_id;
   std::vector<ScoredDocumentHit> scored_document_hits = {
       {document_id1, kSectionIdMaskNone, /*score=*/1},
@@ -450,8 +493,9 @@
   // limiter, so we should get an empty page.
   ICING_ASSERT_OK_AND_ASSIGN(
       PageResultInfo page_result_info2,
-      result_state_manager.GetNextPage(next_page_token, result_retriever(),
-                                       clock()->GetSystemTimeMilliseconds()));
+      result_state_manager.GetNextPage(
+          next_page_token, /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   EXPECT_THAT(page_result_info2.first, Eq(kInvalidNextPageToken));
   EXPECT_THAT(page_result_info2.second.results, IsEmpty());
 }
@@ -518,8 +562,9 @@
   clock()->SetSystemTimeMilliseconds(1000);
   // page_result_info1's token (page_result_info1.first) shouldn't be found.
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info1.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info1.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 }
 
@@ -567,18 +612,21 @@
   // 3. Then calling GetNextPage() on state 1 shouldn't get anything.
   clock()->SetSystemTimeMilliseconds(kDefaultResultStateTtlInMs + 1000);
   // page_result_info2's token (page_result_info2.first) should be found
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info2,
-                             result_state_manager.GetNextPage(
-                                 page_result_info2.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info2,
+      result_state_manager.GetNextPage(
+          page_result_info2.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   // We test the behavior by setting time back to 2s, to make sure the
   // invalidation of state 1 was done by the previous GetNextPage() instead of
   // the following GetNextPage().
   clock()->SetSystemTimeMilliseconds(2000);
   // page_result_info1's token (page_result_info1.first) shouldn't be found.
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info1.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info1.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 }
 
@@ -611,29 +659,42 @@
   clock()->SetSystemTimeMilliseconds(kDefaultResultStateTtlInMs + 1000);
   // page_result_info's token (page_result_info.first) shouldn't be found.
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 }
 
 TEST_F(ResultStateManagerTest, ShouldInvalidateOneToken) {
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store().Put(CreateDocument(/*id=*/1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/1))));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store().Put(CreateDocument(/*id=*/2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/2))));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store().Put(CreateDocument(/*id=*/3)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/3))));
   DocumentId document_id3 = put_result3.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             document_store().Put(CreateDocument(/*id=*/4)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/4))));
   DocumentId document_id4 = put_result4.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result5,
-                             document_store().Put(CreateDocument(/*id=*/5)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result5,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/5))));
   DocumentId document_id5 = put_result5.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result6,
-                             document_store().Put(CreateDocument(/*id=*/6)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result6,
+      document_store().Put(
+          document_util::CreateDocumentWrapper(CreateDocument(/*id=*/6))));
   DocumentId document_id6 = put_result6.new_document_id;
   std::vector<ScoredDocumentHit> scored_document_hits1 = {
       {document_id1, kSectionIdMaskNone, /*score=*/1},
@@ -674,15 +735,18 @@
 
   // page_result_info1's token (page_result_info1.first) shouldn't be found
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info1.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info1.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
   // page_result_info2's token (page_result_info2.first) should still exist
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info2,
-                             result_state_manager.GetNextPage(
-                                 page_result_info2.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info2,
+      result_state_manager.GetNextPage(
+          page_result_info2.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   // Should get docs.
   ASSERT_THAT(page_result_info2.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info2.second.results.at(0).document(),
@@ -724,14 +788,16 @@
 
   // page_result_info1's token (page_result_info1.first) shouldn't be found
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info1.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info1.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
   // page_result_info2's token (page_result_info2.first) shouldn't be found
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info2.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info2.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 }
 
@@ -781,22 +847,27 @@
           clock()->GetSystemTimeMilliseconds()));
 
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info1.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info1.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info2,
-                             result_state_manager.GetNextPage(
-                                 page_result_info2.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info2,
+      result_state_manager.GetNextPage(
+          page_result_info2.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info2.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info2.second.results.at(0).document(),
               EqualsProto(document_protos2.at(1)));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info3,
-                             result_state_manager.GetNextPage(
-                                 page_result_info3.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info3,
+      result_state_manager.GetNextPage(
+          page_result_info3.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info3.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info3.second.results.at(0).document(),
               EqualsProto(document_protos3.at(1)));
@@ -872,31 +943,38 @@
           document_store(), result_retriever(),
           clock()->GetSystemTimeMilliseconds()));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info1,
-                             result_state_manager.GetNextPage(
-                                 page_result_info1.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info1,
+      result_state_manager.GetNextPage(
+          page_result_info1.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info1.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info1.second.results.at(0).document(),
               EqualsProto(document_protos1.at(1)));
 
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info2.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info2.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info3,
-                             result_state_manager.GetNextPage(
-                                 page_result_info3.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info3,
+      result_state_manager.GetNextPage(
+          page_result_info3.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info3.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info3.second.results.at(0).document(),
               EqualsProto(document_protos3.at(1)));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info4,
-                             result_state_manager.GetNextPage(
-                                 page_result_info4.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info4,
+      result_state_manager.GetNextPage(
+          page_result_info4.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info4.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info4.second.results.at(0).document(),
               EqualsProto(document_protos4.at(1)));
@@ -999,40 +1077,49 @@
           clock()->GetSystemTimeMilliseconds()));
 
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info1.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info1.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info2.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info2.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info3.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info3.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info4,
-                             result_state_manager.GetNextPage(
-                                 page_result_info4.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info4,
+      result_state_manager.GetNextPage(
+          page_result_info4.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info4.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info4.second.results.at(0).document(),
               EqualsProto(document_protos4.at(1)));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info5,
-                             result_state_manager.GetNextPage(
-                                 page_result_info5.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info5,
+      result_state_manager.GetNextPage(
+          page_result_info5.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info5.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info5.second.results.at(0).document(),
               EqualsProto(document_protos5.at(1)));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info6,
-                             result_state_manager.GetNextPage(
-                                 page_result_info6.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info6,
+      result_state_manager.GetNextPage(
+          page_result_info6.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info6.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info6.second.results.at(0).document(),
               EqualsProto(document_protos6.at(1)));
@@ -1126,35 +1213,43 @@
           clock()->GetSystemTimeMilliseconds()));
 
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info1.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info1.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info2.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info2.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info3,
-                             result_state_manager.GetNextPage(
-                                 page_result_info3.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info3,
+      result_state_manager.GetNextPage(
+          page_result_info3.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info3.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info3.second.results.at(0).document(),
               EqualsProto(document_protos3.at(1)));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info4,
-                             result_state_manager.GetNextPage(
-                                 page_result_info4.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info4,
+      result_state_manager.GetNextPage(
+          page_result_info4.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info4.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info4.second.results.at(0).document(),
               EqualsProto(document_protos4.at(1)));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info5,
-                             result_state_manager.GetNextPage(
-                                 page_result_info5.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info5,
+      result_state_manager.GetNextPage(
+          page_result_info5.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info5.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info5.second.results.at(0).document(),
               EqualsProto(document_protos5.at(1)));
@@ -1211,10 +1306,12 @@
 
   // GetNextPage for result state 1 should return its result and decrement the
   // number of cached hits to 2.
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info1,
-                             result_state_manager.GetNextPage(
-                                 page_result_info1.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info1,
+      result_state_manager.GetNextPage(
+          page_result_info1.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info1.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info1.second.results.at(0).document(),
               EqualsProto(document_protos1.at(1)));
@@ -1236,30 +1333,37 @@
           clock()->GetSystemTimeMilliseconds()));
 
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info1.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info1.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info2,
-                             result_state_manager.GetNextPage(
-                                 page_result_info2.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info2,
+      result_state_manager.GetNextPage(
+          page_result_info2.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info2.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info2.second.results.at(0).document(),
               EqualsProto(document_protos2.at(1)));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info3,
-                             result_state_manager.GetNextPage(
-                                 page_result_info3.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info3,
+      result_state_manager.GetNextPage(
+          page_result_info3.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info3.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info3.second.results.at(0).document(),
               EqualsProto(document_protos3.at(1)));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info4,
-                             result_state_manager.GetNextPage(
-                                 page_result_info4.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info4,
+      result_state_manager.GetNextPage(
+          page_result_info4.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info4.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info4.second.results.at(0).document(),
               EqualsProto(document_protos4.at(1)));
@@ -1317,10 +1421,12 @@
 
   // GetNextPage for result state 1 should return its result and decrement the
   // number of cached hits to 2.
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info1,
-                             result_state_manager.GetNextPage(
-                                 page_result_info1.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info1,
+      result_state_manager.GetNextPage(
+          page_result_info1.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info1.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info1.second.results.at(0).document(),
               EqualsProto(document_protos1.at(1)));
@@ -1358,35 +1464,43 @@
           clock()->GetSystemTimeMilliseconds()));
 
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info1.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info1.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info2.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info2.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info3,
-                             result_state_manager.GetNextPage(
-                                 page_result_info3.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info3,
+      result_state_manager.GetNextPage(
+          page_result_info3.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info3.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info3.second.results.at(0).document(),
               EqualsProto(document_protos3.at(1)));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info4,
-                             result_state_manager.GetNextPage(
-                                 page_result_info4.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info4,
+      result_state_manager.GetNextPage(
+          page_result_info4.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info4.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info4.second.results.at(0).document(),
               EqualsProto(document_protos4.at(1)));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info5,
-                             result_state_manager.GetNextPage(
-                                 page_result_info5.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info5,
+      result_state_manager.GetNextPage(
+          page_result_info5.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info5.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info5.second.results.at(0).document(),
               EqualsProto(document_protos5.at(1)));
@@ -1435,6 +1549,7 @@
   auto [scored_document_hits3, document_protos3] = AddScoredDocuments(
       {/*document_id=*/5, /*document_id=*/6, /*document_id=*/7,
        /*document_id=*/8, /*document_id=*/9, /*document_id=*/10});
+  QueryStatsProto query_stats;
   ICING_ASSERT_OK_AND_ASSIGN(
       PageResultInfo page_result_info3,
       result_state_manager.CacheAndRetrieveFirstPage(
@@ -1444,26 +1559,32 @@
           /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr,
           CreateResultSpec(/*num_per_page=*/1, ResultSpecProto::NAMESPACE),
           document_store(), result_retriever(),
-          clock()->GetSystemTimeMilliseconds()));
+          clock()->GetSystemTimeMilliseconds(), &query_stats));
   EXPECT_THAT(page_result_info3.first, Not(Eq(kInvalidNextPageToken)));
+  // Should set num_result_states_evicted since result state 1 and 2 were
+  // evicted.
+  EXPECT_THAT(query_stats.num_result_states_evicted(), Eq(2));
 
   // GetNextPage for result state 1 and 2 should return NOT_FOUND.
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info1.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info1.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info2.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info2.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
   // Only the next four results in state 3 should be retrievable.
   uint64_t next_page_token3 = page_result_info3.first;
   ICING_ASSERT_OK_AND_ASSIGN(
       page_result_info3,
-      result_state_manager.GetNextPage(next_page_token3, result_retriever(),
-                                       clock()->GetSystemTimeMilliseconds()));
+      result_state_manager.GetNextPage(
+          next_page_token3, /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   EXPECT_THAT(page_result_info3.first, Eq(next_page_token3));
   ASSERT_THAT(page_result_info3.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info3.second.results.at(0).document(),
@@ -1471,8 +1592,9 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       page_result_info3,
-      result_state_manager.GetNextPage(next_page_token3, result_retriever(),
-                                       clock()->GetSystemTimeMilliseconds()));
+      result_state_manager.GetNextPage(
+          next_page_token3, /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   EXPECT_THAT(page_result_info3.first, Eq(next_page_token3));
   ASSERT_THAT(page_result_info3.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info3.second.results.at(0).document(),
@@ -1480,8 +1602,9 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       page_result_info3,
-      result_state_manager.GetNextPage(next_page_token3, result_retriever(),
-                                       clock()->GetSystemTimeMilliseconds()));
+      result_state_manager.GetNextPage(
+          next_page_token3, /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   EXPECT_THAT(page_result_info3.first, Eq(next_page_token3));
   ASSERT_THAT(page_result_info3.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info3.second.results.at(0).document(),
@@ -1489,8 +1612,9 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       page_result_info3,
-      result_state_manager.GetNextPage(next_page_token3, result_retriever(),
-                                       clock()->GetSystemTimeMilliseconds()));
+      result_state_manager.GetNextPage(
+          next_page_token3, /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   // The final document should have been dropped because it exceeded the budget,
   // so the next page token of the second last round should be
   // kInvalidNextPageToken.
@@ -1501,8 +1625,9 @@
 
   // Double check that next_page_token3 is not retrievable anymore.
   EXPECT_THAT(
-      result_state_manager.GetNextPage(next_page_token3, result_retriever(),
-                                       clock()->GetSystemTimeMilliseconds()),
+      result_state_manager.GetNextPage(
+          next_page_token3, /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()),
       StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 }
 
@@ -1545,14 +1670,17 @@
 
   // state1 should have been evicted and state2 should still be retrievable.
   EXPECT_THAT(result_state_manager.GetNextPage(
-                  page_result_info1.first, result_retriever(),
-                  clock()->GetSystemTimeMilliseconds()),
+                  page_result_info1.first,
+                  /*max_results=*/std::numeric_limits<int32_t>::max(),
+                  result_retriever(), clock()->GetSystemTimeMilliseconds()),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
-  ICING_ASSERT_OK_AND_ASSIGN(page_result_info2,
-                             result_state_manager.GetNextPage(
-                                 page_result_info2.first, result_retriever(),
-                                 clock()->GetSystemTimeMilliseconds()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      page_result_info2,
+      result_state_manager.GetNextPage(
+          page_result_info2.first,
+          /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info2.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info2.second.results.at(0).document(),
               EqualsProto(document_protos2.at(1)));
@@ -1596,8 +1724,9 @@
   // Second page, 2 results.
   ICING_ASSERT_OK_AND_ASSIGN(
       PageResultInfo page_result_info2,
-      result_state_manager.GetNextPage(next_page_token, result_retriever(),
-                                       clock()->GetSystemTimeMilliseconds()));
+      result_state_manager.GetNextPage(
+          next_page_token, /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info2.second.results, SizeIs(2));
   EXPECT_THAT(page_result_info2.second.results.at(0).document(),
               EqualsProto(document_protos.at(2)));
@@ -1607,19 +1736,128 @@
   // Third page, 1 result.
   ICING_ASSERT_OK_AND_ASSIGN(
       PageResultInfo page_result_info3,
-      result_state_manager.GetNextPage(next_page_token, result_retriever(),
-                                       clock()->GetSystemTimeMilliseconds()));
+      result_state_manager.GetNextPage(
+          next_page_token, /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()));
   ASSERT_THAT(page_result_info3.second.results, SizeIs(1));
   EXPECT_THAT(page_result_info3.second.results.at(0).document(),
               EqualsProto(document_protos.at(4)));
 
   // Fourth page, 0 results.
   EXPECT_THAT(
-      result_state_manager.GetNextPage(next_page_token, result_retriever(),
-                                       clock()->GetSystemTimeMilliseconds()),
+      result_state_manager.GetNextPage(
+          next_page_token, /*max_results=*/std::numeric_limits<int32_t>::max(),
+          result_retriever(), clock()->GetSystemTimeMilliseconds()),
       StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 }
 
+TEST_F(ResultStateManagerTest, GetNumActiveResultStates) {
+  auto [scored_document_hits1, document_protos1] = AddScoredDocuments(
+      {/*document_id=*/0, /*document_id=*/1, /*document_id=*/2});
+  auto [scored_document_hits2, document_protos2] = AddScoredDocuments(
+      {/*document_id=*/3, /*document_id=*/4, /*document_id=*/5});
+  auto [scored_document_hits3, document_protos3] = AddScoredDocuments(
+      {/*document_id=*/6, /*document_id=*/7, /*document_id=*/8});
+
+  ResultStateManager result_state_manager(
+      /*max_total_hits=*/std::numeric_limits<int>::max(), document_store());
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      PageResultInfo page_result_info1,
+      result_state_manager.CacheAndRetrieveFirstPage(
+          std::make_unique<
+              PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>(
+              std::move(scored_document_hits1), /*is_descending=*/true),
+          /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr,
+          CreateResultSpec(/*num_per_page=*/1, ResultSpecProto::NAMESPACE),
+          document_store(), result_retriever(),
+          clock()->GetSystemTimeMilliseconds()));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      PageResultInfo page_result_info2,
+      result_state_manager.CacheAndRetrieveFirstPage(
+          std::make_unique<
+              PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>(
+              std::move(scored_document_hits2), /*is_descending=*/true),
+          /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr,
+          CreateResultSpec(/*num_per_page=*/1, ResultSpecProto::NAMESPACE),
+          document_store(), result_retriever(),
+          clock()->GetSystemTimeMilliseconds()));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      PageResultInfo page_result_info3,
+      result_state_manager.CacheAndRetrieveFirstPage(
+          std::make_unique<
+              PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>(
+              std::move(scored_document_hits3), /*is_descending=*/true),
+          /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr,
+          CreateResultSpec(/*num_per_page=*/1, ResultSpecProto::NAMESPACE),
+          document_store(), result_retriever(),
+          clock()->GetSystemTimeMilliseconds()));
+
+  EXPECT_THAT(result_state_manager.GetNumActiveResultStates(
+                  /*current_time_ms=*/clock()->GetSystemTimeMilliseconds()),
+              Eq(3));
+}
+
+TEST_F(ResultStateManagerTest,
+       GetNumActiveResultStatesShouldInvalidateExpiredResultStates) {
+  auto [scored_document_hits1, document_protos1] = AddScoredDocuments(
+      {/*document_id=*/0, /*document_id=*/1, /*document_id=*/2});
+  auto [scored_document_hits2, document_protos2] = AddScoredDocuments(
+      {/*document_id=*/3, /*document_id=*/4, /*document_id=*/5});
+  auto [scored_document_hits3, document_protos3] = AddScoredDocuments(
+      {/*document_id=*/6, /*document_id=*/7, /*document_id=*/8});
+
+  ResultStateManager result_state_manager(
+      /*max_total_hits=*/std::numeric_limits<int>::max(), document_store());
+
+  // Set time as 1s and add state.
+  clock()->SetSystemTimeMilliseconds(1000);
+  ICING_ASSERT_OK_AND_ASSIGN(
+      PageResultInfo page_result_info1,
+      result_state_manager.CacheAndRetrieveFirstPage(
+          std::make_unique<
+              PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>(
+              std::move(scored_document_hits1), /*is_descending=*/true),
+          /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr,
+          CreateResultSpec(/*num_per_page=*/1, ResultSpecProto::NAMESPACE),
+          document_store(), result_retriever(),
+          clock()->GetSystemTimeMilliseconds()));
+
+  // Set time as 10s and add state 2, state 3.
+  clock()->SetSystemTimeMilliseconds(10000);
+  ICING_ASSERT_OK_AND_ASSIGN(
+      PageResultInfo page_result_info2,
+      result_state_manager.CacheAndRetrieveFirstPage(
+          std::make_unique<
+              PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>(
+              std::move(scored_document_hits2), /*is_descending=*/true),
+          /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr,
+          CreateResultSpec(/*num_per_page=*/1, ResultSpecProto::NAMESPACE),
+          document_store(), result_retriever(),
+          clock()->GetSystemTimeMilliseconds()));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      PageResultInfo page_result_info3,
+      result_state_manager.CacheAndRetrieveFirstPage(
+          std::make_unique<
+              PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>>(
+              std::move(scored_document_hits3), /*is_descending=*/true),
+          /*parent_adjustment_info=*/nullptr, /*child_adjustment_info=*/nullptr,
+          CreateResultSpec(/*num_per_page=*/1, ResultSpecProto::NAMESPACE),
+          document_store(), result_retriever(),
+          clock()->GetSystemTimeMilliseconds()));
+
+  // 1. Set time as 1hr1s.
+  // 2. Then calling GetNumActiveResultStates() should invalidate state 1 and
+  //    return 2.
+  clock()->SetSystemTimeMilliseconds(kDefaultResultStateTtlInMs + 1000);
+  EXPECT_THAT(result_state_manager.GetNumActiveResultStates(
+                  /*current_time_ms=*/clock()->GetSystemTimeMilliseconds()),
+              Eq(2));
+}
+
 }  // namespace
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/result/result-state-manager_thread-safety_test.cc b/icing/result/result-state-manager_thread-safety_test.cc
index 5578c54..fd82de5 100644
--- a/icing/result/result-state-manager_thread-safety_test.cc
+++ b/icing/result/result-state-manager_thread-safety_test.cc
@@ -17,8 +17,13 @@
 #include <limits>
 #include <memory>
 #include <optional>
+#include <string>
 #include <thread>  // NOLINT
+#include <utility>
+#include <vector>
 
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "icing/document-builder.h"
@@ -26,11 +31,15 @@
 #include "icing/file/filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/portable/equals-proto.h"
+#include "icing/portable/gzip_stream.h"
+#include "icing/portable/platform.h"
 #include "icing/result/page-result.h"
 #include "icing/result/result-retriever-v2.h"
 #include "icing/result/result-state-manager.h"
 #include "icing/schema/schema-store.h"
+#include "icing/schema/section.h"
 #include "icing/scoring/priority-queue-scored-document-hits-ranker.h"
+#include "icing/scoring/scored-document-hit.h"
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
@@ -38,10 +47,10 @@
 #include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 #include "icing/tokenization/language-segmenter-factory.h"
+#include "icing/tokenization/language-segmenter.h"
 #include "icing/transform/normalizer-factory.h"
 #include "icing/transform/normalizer-options.h"
 #include "icing/transform/normalizer.h"
-#include "icing/util/clock.h"
 #include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
@@ -61,14 +70,17 @@
   return result_spec;
 }
 
-DocumentProto CreateDocument(int document_id) {
-  return DocumentBuilder()
-      .SetNamespace("namespace")
-      .SetUri(std::to_string(document_id))
-      .SetSchema("Document")
-      .SetCreationTimestampMs(1574365086666 + document_id)
-      .SetScore(document_id)
-      .Build();
+DocumentWrapper CreateDocument(int document_id) {
+  DocumentWrapper document_wrapper;
+  *document_wrapper.mutable_document() =
+      DocumentBuilder()
+          .SetNamespace("namespace")
+          .SetUri(std::to_string(document_id))
+          .SetSchema("Document")
+          .SetCreationTimestampMs(1574365086666 + document_id)
+          .SetScore(document_id)
+          .Build();
+  return document_wrapper;
 }
 
 class ResultStateManagerThreadSafetyTest : public testing::Test {
@@ -109,20 +121,25 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult result,
-        DocumentStore::Create(&filesystem_, test_dir_, clock_.get(),
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, test_dir_, clock_.get(), schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(result.document_store);
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        result_retriever_, ResultRetrieverV2::Create(
-                               document_store_.get(), schema_store_.get(),
-                               language_segmenter_.get(), normalizer_.get()));
+        result_retriever_,
+        ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
+                                  language_segmenter_.get(), normalizer_.get(),
+                                  feature_flags_.get()));
   }
 
   void TearDown() override {
@@ -194,12 +211,14 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<ResultRetrieverV2> result_retriever,
         ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                  language_segmenter_.get(),
-                                  normalizer_.get()));
+                                  language_segmenter_.get(), normalizer_.get(),
+                                  feature_flags_.get()));
     ICING_ASSERT_OK_AND_ASSIGN(
         PageResultInfo page_result_info,
-        result_state_manager.GetNextPage(next_page_token, *result_retriever,
-                                         clock_->GetSystemTimeMilliseconds()));
+        result_state_manager.GetNextPage(
+            next_page_token,
+            /*max_results=*/std::numeric_limits<int32_t>::max(),
+            *result_retriever, clock_->GetSystemTimeMilliseconds()));
     page_results[thread_id] =
         std::make_optional<PageResultInfo>(std::move(page_result_info));
   };
@@ -297,12 +316,14 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<ResultRetrieverV2> result_retriever,
         ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                  language_segmenter_.get(),
-                                  normalizer_.get()));
+                                  language_segmenter_.get(), normalizer_.get(),
+                                  feature_flags_.get()));
 
     libtextclassifier3::StatusOr<PageResultInfo> page_result_info_or =
-        result_state_manager.GetNextPage(next_page_token, *result_retriever,
-                                         clock_->GetSystemTimeMilliseconds());
+        result_state_manager.GetNextPage(
+            next_page_token,
+            /*max_results=*/std::numeric_limits<int32_t>::max(),
+            *result_retriever, clock_->GetSystemTimeMilliseconds());
     if (page_result_info_or.ok()) {
       page_results[thread_id] = std::make_optional<PageResultInfo>(
           std::move(page_result_info_or).ValueOrDie());
@@ -392,8 +413,8 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<ResultRetrieverV2> result_retriever,
         ResultRetrieverV2::Create(document_store_.get(), schema_store_.get(),
-                                  language_segmenter_.get(),
-                                  normalizer_.get()));
+                                  language_segmenter_.get(), normalizer_.get(),
+                                  feature_flags_.get()));
 
     // Retrieve the first page.
     // Documents are ordered by score *ascending*, so the first page should
@@ -428,10 +449,12 @@
     // each thread should retrieve 1, 2, 3, ..., kNumThreads pages.
     int num_subsequent_pages_to_retrieve = thread_id;
     for (int i = 0; i < num_subsequent_pages_to_retrieve; ++i) {
-      ICING_ASSERT_OK_AND_ASSIGN(PageResultInfo page_result_info,
-                                 result_state_manager.GetNextPage(
-                                     next_page_token, *result_retriever,
-                                     clock_->GetSystemTimeMilliseconds()));
+      ICING_ASSERT_OK_AND_ASSIGN(
+          PageResultInfo page_result_info,
+          result_state_manager.GetNextPage(
+              next_page_token,
+              /*max_results=*/std::numeric_limits<int32_t>::max(),
+              *result_retriever, clock_->GetSystemTimeMilliseconds()));
       EXPECT_THAT(page_result_info.second.results, SizeIs(kNumPerPage));
       for (int j = 0; j < kNumPerPage; ++j) {
         EXPECT_THAT(page_result_info.second.results[j].score(),
diff --git a/icing/result/result-state-v2.cc b/icing/result/result-state-v2.cc
index 3aa9359..e7248bf 100644
--- a/icing/result/result-state-v2.cc
+++ b/icing/result/result-state-v2.cc
@@ -17,7 +17,8 @@
 #include <atomic>
 #include <cstdint>
 #include <memory>
-#include <string>
+#include <optional>
+#include <utility>
 #include <vector>
 
 #include "icing/proto/search.pb.h"
@@ -50,15 +51,12 @@
     group_result_limits.push_back(result_grouping.max_results());
     for (const ResultSpecProto::ResultGrouping::Entry& entry :
          result_grouping.entry_groupings()) {
-      const std::string& name_space = entry.namespace_();
-      const std::string& schema = entry.schema();
-      auto entry_id_or = document_store.GetResultGroupingEntryId(
-          result_group_type_, name_space, schema);
-      if (!entry_id_or.ok()) {
+      std::optional<int32_t> entry_id = document_store.GetResultGroupingEntryId(
+          result_group_type_, entry.namespace_(), entry.schema());
+      if (!entry_id.has_value()) {
         continue;
       }
-      int32_t entry_id = entry_id_or.ValueOrDie();
-      entry_id_group_id_map_.insert({entry_id, group_id});
+      entry_id_group_id_map_.insert({*entry_id, group_id});
     }
   }
 }
diff --git a/icing/result/result-state-v2_test.cc b/icing/result/result-state-v2_test.cc
index 6b8b4bd..fb3dea4 100644
--- a/icing/result/result-state-v2_test.cc
+++ b/icing/result/result-state-v2_test.cc
@@ -25,9 +25,11 @@
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "icing/absl_ports/mutex.h"
+#include "icing/document-builder.h"
 #include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/document_wrapper.pb.h"
 #include "icing/proto/schema.pb.h"
@@ -79,14 +81,18 @@
     filesystem_.CreateDirectoryRecursively(doc_store_base_dir_.c_str());
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult result,
-        DocumentStore::Create(&filesystem_, doc_store_base_dir_, &clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, doc_store_base_dir_, &clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(result.document_store);
 
     num_total_hits_ = 0;
@@ -102,7 +108,11 @@
     document.set_namespace_("namespace");
     document.set_uri(std::to_string(document_id));
     document.set_schema("Document");
-    document_store_->Put(std::move(document));
+
+    DocumentWrapper document_wrapper;
+    *document_wrapper.mutable_document() = std::move(document);
+
+    document_store_->Put(document_wrapper);
     return ScoredDocumentHit(document_id, kSectionIdMaskNone, /*score=*/1);
   }
 
@@ -177,23 +187,29 @@
 TEST_F(ResultStateV2Test,
        ShouldConstructNamespaceGroupIdMapAndGroupResultLimitsAccordingToSpecs) {
   // Create 3 docs under namespace1, namespace2, namespace3.
-  DocumentProto document1;
-  document1.set_namespace_("namespace1");
-  document1.set_uri("uri/1");
-  document1.set_schema("Document");
-  ICING_ASSERT_OK(document_store().Put(std::move(document1)));
+  DocumentWrapper document_wrapper1;
+  *document_wrapper1.mutable_document() = DocumentBuilder()
+                                              .SetNamespace("namespace1")
+                                              .SetUri("uri/1")
+                                              .SetSchema("Document")
+                                              .Build();
+  ICING_ASSERT_OK(document_store().Put(document_wrapper1));
 
-  DocumentProto document2;
-  document2.set_namespace_("namespace2");
-  document2.set_uri("uri/2");
-  document2.set_schema("Document");
-  ICING_ASSERT_OK(document_store().Put(std::move(document2)));
+  DocumentWrapper document_wrapper2;
+  *document_wrapper2.mutable_document() = DocumentBuilder()
+                                              .SetNamespace("namespace2")
+                                              .SetUri("uri/2")
+                                              .SetSchema("Document")
+                                              .Build();
+  ICING_ASSERT_OK(document_store().Put(document_wrapper2));
 
-  DocumentProto document3;
-  document3.set_namespace_("namespace3");
-  document3.set_uri("uri/3");
-  document3.set_schema("Document");
-  ICING_ASSERT_OK(document_store().Put(std::move(document3)));
+  DocumentWrapper document_wrapper3;
+  *document_wrapper3.mutable_document() = DocumentBuilder()
+                                              .SetNamespace("namespace3")
+                                              .SetUri("uri/3")
+                                              .SetSchema("Document")
+                                              .Build();
+  ICING_ASSERT_OK(document_store().Put(document_wrapper3));
 
   // Create a ResultSpec that limits "namespace1" to 3 results and limits
   // "namespace2"+"namespace3" to a total of 2 results. Also add
@@ -222,13 +238,13 @@
   entry->set_namespace_("nonexistentNamespace1");
 
   // Get entry ids.
-  ICING_ASSERT_OK_AND_ASSIGN(
+  ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
       int32_t entry_id1, document_store().GetResultGroupingEntryId(
                              result_grouping_type, "namespace1", "Document"));
-  ICING_ASSERT_OK_AND_ASSIGN(
+  ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
       int32_t entry_id2, document_store().GetResultGroupingEntryId(
                              result_grouping_type, "namespace2", "Document"));
-  ICING_ASSERT_OK_AND_ASSIGN(
+  ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
       int32_t entry_id3, document_store().GetResultGroupingEntryId(
                              result_grouping_type, "namespace3", "Document"));
 
diff --git a/icing/result/snippet-retriever.cc b/icing/result/snippet-retriever.cc
index 59552c8..7806ae3 100644
--- a/icing/result/snippet-retriever.cc
+++ b/icing/result/snippet-retriever.cc
@@ -512,9 +512,10 @@
     CharacterIterator start_itr(value);
     CharacterIterator end_itr(value);
     CharacterIterator reset_itr(value);
+    std::vector<Token> batch_tokens;
     bool encountered_error = false;
     while (iterator->Advance()) {
-      std::vector<Token> batch_tokens = iterator->GetTokens();
+      iterator->GetTokens(&batch_tokens);
       if (batch_tokens.empty()) {
         continue;
       }
diff --git a/icing/result/snippet-retriever_test.cc b/icing/result/snippet-retriever_test.cc
index 45ee1ec..927b419 100644
--- a/icing/result/snippet-retriever_test.cc
+++ b/icing/result/snippet-retriever_test.cc
@@ -2224,12 +2224,12 @@
   EXPECT_THAT(snippet.entries(0).property_name(), Eq("embedding1[0]"));
   EXPECT_THAT(
       snippet.entries(0).embedding_matches(),
-      UnorderedElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
+      ElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
           /*score=*/0.5, /*query_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT))));
   EXPECT_THAT(snippet.entries(1).property_name(), Eq("embedding1[2]"));
   EXPECT_THAT(
       snippet.entries(1).embedding_matches(),
-      UnorderedElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
+      ElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
           /*score=*/0.6, /*query_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT))));
   EXPECT_THAT(snippet.entries(2).property_name(), Eq("embedding1[3]"));
   EXPECT_THAT(
@@ -2241,7 +2241,7 @@
               /*score=*/-0.4, /*query_index=*/3, EMBEDDING_METRIC_COSINE))));
   EXPECT_THAT(snippet.entries(3).property_name(), Eq("embedding1[4]"));
   EXPECT_THAT(snippet.entries(3).embedding_matches(),
-              UnorderedElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
+              ElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
                   /*score=*/3, /*query_index=*/2, EMBEDDING_METRIC_COSINE))));
 
   // Section 1 matches
@@ -2256,7 +2256,7 @@
   EXPECT_THAT(snippet.entries(5).property_name(), Eq("embedding2[2]"));
   EXPECT_THAT(
       snippet.entries(5).embedding_matches(),
-      UnorderedElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
+      ElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
           /*score=*/1, /*query_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT))));
 }
 
@@ -2376,12 +2376,12 @@
   EXPECT_THAT(snippet.entries(0).property_name(), Eq("embedding1[0]"));
   EXPECT_THAT(
       snippet.entries(0).embedding_matches(),
-      UnorderedElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
+      ElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
           /*score=*/0.5, /*query_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT))));
   EXPECT_THAT(snippet.entries(1).property_name(), Eq("embedding1[2]"));
   EXPECT_THAT(
       snippet.entries(1).embedding_matches(),
-      UnorderedElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
+      ElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
           /*score=*/0.6, /*query_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT))));
   EXPECT_THAT(snippet.entries(2).property_name(), Eq("embedding1[3]"));
   EXPECT_THAT(
@@ -2393,7 +2393,7 @@
               /*score=*/-0.4, /*query_index=*/3, EMBEDDING_METRIC_COSINE))));
   EXPECT_THAT(snippet.entries(3).property_name(), Eq("embedding1[4]"));
   EXPECT_THAT(snippet.entries(3).embedding_matches(),
-              UnorderedElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
+              ElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
                   /*score=*/3, /*query_index=*/2, EMBEDDING_METRIC_COSINE))));
 
   // Section 1 matches
@@ -2408,7 +2408,7 @@
   EXPECT_THAT(snippet.entries(5).property_name(), Eq("embedding2[2]"));
   EXPECT_THAT(
       snippet.entries(5).embedding_matches(),
-      UnorderedElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
+      ElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
           /*score=*/1, /*query_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT))));
 
   // Document 1 has no matches
@@ -2559,12 +2559,12 @@
   EXPECT_THAT(snippet.entries(1).property_name(), Eq("embedding1[0]"));
   EXPECT_THAT(
       snippet.entries(1).embedding_matches(),
-      UnorderedElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
+      ElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
           /*score=*/0.5, /*query_index=*/0, EMBEDDING_METRIC_DOT_PRODUCT))));
   EXPECT_THAT(snippet.entries(2).property_name(), Eq("embedding1[2]"));
   EXPECT_THAT(
       snippet.entries(2).embedding_matches(),
-      UnorderedElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
+      ElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
           /*score=*/0.6, /*query_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT))));
   EXPECT_THAT(snippet.entries(3).property_name(), Eq("embedding1[3]"));
   EXPECT_THAT(
@@ -2576,7 +2576,7 @@
               /*score=*/-0.4, /*query_index=*/3, EMBEDDING_METRIC_COSINE))));
   EXPECT_THAT(snippet.entries(4).property_name(), Eq("embedding1[4]"));
   EXPECT_THAT(snippet.entries(4).embedding_matches(),
-              UnorderedElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
+              ElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
                   /*score=*/3, /*query_index=*/2, EMBEDDING_METRIC_COSINE))));
 
   // embedding2 matches
@@ -2591,7 +2591,7 @@
   EXPECT_THAT(snippet.entries(6).property_name(), Eq("embedding2[2]"));
   EXPECT_THAT(
       snippet.entries(6).embedding_matches(),
-      UnorderedElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
+      ElementsAre(EqualsProto(CreateEmbeddingMatchSnippetProto(
           /*score=*/1, /*query_index=*/1, EMBEDDING_METRIC_DOT_PRODUCT))));
 
   // 'subject' text matches
diff --git a/icing/schema/backup-schema-producer_test.cc b/icing/schema/backup-schema-producer_test.cc
index d0f609c..8b1d124 100644
--- a/icing/schema/backup-schema-producer_test.cc
+++ b/icing/schema/backup-schema-producer_test.cc
@@ -945,21 +945,50 @@
 
 INSTANTIATE_TEST_SUITE_P(
     BackupSchemaProducerTest, BackupSchemaProducerTest,
-    testing::Values(
-        FeatureFlags(/*allow_circular_schema_definitions=*/true,
-                     /*enable_scorable_properties=*/true,
-                     /*enable_embedding_quantization=*/true,
-                     /*enable_repeated_field_joins=*/true,
-                     /*enable_embedding_backup_generation=*/false,
-                     /*enable_schema_database=*/true,
-                     /*release_backup_schema_file_if_overlay_present=*/true),
-        FeatureFlags(/*allow_circular_schema_definitions=*/true,
-                     /*enable_scorable_properties=*/true,
-                     /*enable_embedding_quantization=*/true,
-                     /*enable_repeated_field_joins=*/true,
-                     /*enable_embedding_backup_generation=*/true,
-                     /*enable_schema_database=*/true,
-                     /*release_backup_schema_file_if_overlay_present=*/true)));
+    testing::Values(FeatureFlags(
+                        /*allow_circular_schema_definitions=*/true,
+                        /*enable_scorable_properties=*/true,
+                        /*enable_embedding_quantization=*/true,
+                        /*enable_repeated_field_joins=*/true,
+                        /*enable_embedding_backup_generation=*/false,
+                        /*enable_schema_database=*/true,
+                        /*release_backup_schema_file_if_overlay_present=*/true,
+                        /*enable_strict_page_byte_size=*/true,
+                        /*enable_smaller_decompression_buffer_size=*/true,
+                        /*enable_eigen_embedding_scoring=*/true,
+                        /*enable_passing_filter_to_children=*/true,
+                        /*enable_proto_log_new_header_format=*/true,
+                        /*enable_embedding_iterator_v2=*/true,
+                        /*enable_reusable_decompression_buffer=*/true,
+                        /*enable_schema_type_id_optimization=*/true,
+                        /*enable_optimize_improvements=*/true,
+                        /*expired_document_purge_threshold_ms=*/0,
+                        /*enable_non_existent_qualified_id_join=*/true,
+                        /*enable_skip_set_schema_type_equality_check=*/true,
+                        /*enable_embed_query_optimization=*/true,
+                        /*enable_schema_definition_deduping=*/true),
+                    FeatureFlags(
+                        /*allow_circular_schema_definitions=*/true,
+                        /*enable_scorable_properties=*/true,
+                        /*enable_embedding_quantization=*/true,
+                        /*enable_repeated_field_joins=*/true,
+                        /*enable_embedding_backup_generation=*/true,
+                        /*enable_schema_database=*/true,
+                        /*release_backup_schema_file_if_overlay_present=*/true,
+                        /*enable_strict_page_byte_size=*/true,
+                        /*enable_smaller_decompression_buffer_size=*/true,
+                        /*enable_eigen_embedding_scoring=*/true,
+                        /*enable_passing_filter_to_children=*/true,
+                        /*enable_proto_log_new_header_format=*/true,
+                        /*enable_embedding_iterator_v2=*/true,
+                        /*enable_reusable_decompression_buffer=*/true,
+                        /*enable_schema_type_id_optimization=*/true,
+                        /*enable_optimize_improvements=*/true,
+                        /*expired_document_purge_threshold_ms=*/0,
+                        /*enable_non_existent_qualified_id_join=*/true,
+                        /*enable_skip_set_schema_type_equality_check=*/true,
+                        /*enable_embed_query_optimization=*/true,
+                        /*enable_schema_definition_deduping=*/true)));
 
 }  // namespace
 
diff --git a/icing/schema/schema-store.cc b/icing/schema/schema-store.cc
index dda7746..537ea6f 100644
--- a/icing/schema/schema-store.cc
+++ b/icing/schema/schema-store.cc
@@ -141,7 +141,7 @@
   size_t db_index = schema_type.find(database_delimeter);
   std::string database;
   if (db_index != std::string::npos) {
-    database = schema_type.substr(0, db_index);
+    database = schema_type.substr(0, db_index + 1);
   }
   return database;
 }
@@ -222,24 +222,32 @@
   int64_t file_size = filesystem->GetFileSize(sfd.get());
   if (file_size == sizeof(LegacyHeader)) {
     LegacyHeader legacy_header;
-    if (!filesystem->Read(sfd.get(), &legacy_header, sizeof(legacy_header))) {
+    if (filesystem->Read(sfd.get(), &legacy_header, sizeof(legacy_header)) !=
+        sizeof(legacy_header)) {
       return absl_ports::InternalError(
           absl_ports::StrCat("Couldn't read: ", path));
     }
     if (legacy_header.magic != Header::kMagic) {
+      ICING_LOG(ERROR)
+          << "Invalid legacy header magic for SchemaStore. Expected: "
+          << Header::kMagic << ", actual: " << legacy_header.magic;
       return absl_ports::InternalError(
-          absl_ports::StrCat("Invalid header kMagic for file: ", path));
+          "Invalid legacy header magic for SchemaStore");
     }
     serialized_header.checksum = legacy_header.checksum;
   } else if (file_size == sizeof(SerializedHeader)) {
-    if (!filesystem->Read(sfd.get(), &serialized_header,
-                          sizeof(serialized_header))) {
+    if (filesystem->Read(sfd.get(), &serialized_header,
+                         sizeof(serialized_header)) !=
+        sizeof(serialized_header)) {
       return absl_ports::InternalError(
           absl_ports::StrCat("Couldn't read: ", path));
     }
     if (serialized_header.magic != Header::kMagic) {
+      ICING_LOG(ERROR)
+          << "Invalid serialized header magic for SchemaStore. Expected: "
+          << Header::kMagic << ", actual: " << serialized_header.magic;
       return absl_ports::InternalError(
-          absl_ports::StrCat("Invalid header kMagic for file: ", path));
+          "Invalid serialized header magic for SchemaStore");
     }
   } else if (file_size != 0) {
     // file is neither the legacy header, the new header nor empty. Something is
@@ -558,9 +566,10 @@
   bool overlay_schema_file_exists =
       filesystem_->FileExists(overlay_schema_filename.c_str());
 
+  // The base schema file will be released at a later point (if necessary),
+  // after InitializeInternal is done.
   libtextclassifier3::Status base_schema_state = schema_file_.Read().status();
   if (!base_schema_state.ok() && !absl_ports::IsNotFound(base_schema_state)) {
-    ResetSchemaFileIfNeeded();
     return base_schema_state;
   }
 
@@ -568,7 +577,6 @@
   // 1. Everything is missing. This is an empty schema store.
   if (!base_schema_state.ok() && !overlay_schema_file_exists &&
       !header_exists) {
-    ResetSchemaFileIfNeeded();
     return libtextclassifier3::Status::OK;
   }
 
@@ -577,7 +585,6 @@
   if (base_schema_state.ok() && !overlay_schema_file_exists && header_exists &&
       !header_->overlay_created()) {
     // Nothing else to do. Just return safely.
-    ResetSchemaFileIfNeeded();
     return libtextclassifier3::Status::OK;
   }
 
@@ -587,7 +594,6 @@
       header_->overlay_created()) {
     overlay_schema_file_ = std::make_unique<FileBackedProto<SchemaProto>>(
         *filesystem_, MakeOverlaySchemaFilename(base_dir_));
-    ResetSchemaFileIfNeeded();
     return libtextclassifier3::Status::OK;
   }
 
@@ -595,7 +601,6 @@
   // Return an error.
   bool overlay_created = header_->overlay_created();
   bool base_schema_exists = base_schema_state.ok();
-  ResetSchemaFileIfNeeded();
   return absl_ports::InternalError(IcingStringUtil::StringPrintf(
       "Unable to properly load schema. Header {exists:%d, overlay_created:%d}, "
       "base schema exists: %d, overlay_schema_exists: %d",
@@ -625,6 +630,7 @@
     initialize_stats->set_num_schema_types(type_config_map_.size());
   }
   has_schema_successfully_set_ = true;
+  ResetSchemaFileIfNeeded();
 
   return libtextclassifier3::Status::OK;
 }
@@ -694,7 +700,6 @@
 
   // Write the header
   ICING_RETURN_IF_ERROR(UpdateChecksum());
-  ResetSchemaFileIfNeeded();
   return libtextclassifier3::Status::OK;
 }
 
@@ -705,6 +710,7 @@
       SchemaUtil::BuildTransitiveInheritanceGraph(*schema_proto));
 
   reverse_schema_type_mapper_.clear();
+  reverse_schema_type_mapper_hash_.clear();
   database_type_map_.clear();
   type_config_map_.clear();
   schema_subtype_id_map_.clear();
@@ -716,6 +722,8 @@
 
     // Build reverse_schema_type_mapper_
     reverse_schema_type_mapper_.insert({type_id, type_name});
+    reverse_schema_type_mapper_hash_.insert(
+        {type_id, GetSchemaNameHash(type_name)});
 
     // Build database_type_map_
     database_type_map_[database].push_back(type_name);
@@ -780,7 +788,8 @@
   // schema (both of which will have a checksum of 0). For existing, but empty
   // schemas, we need to continue with the checksum calculation of the other
   // components.
-  if (schema_checksum == Crc32() && !has_schema_successfully_set_) {
+  if (schema_checksum == Crc32() &&
+      absl_ports::IsNotFound(schema_file_.Read().status())) {
     return schema_checksum;
   }
 
@@ -805,7 +814,8 @@
   // schema (both of which will have a checksum of 0). For existing, but empty
   // schemas, we need to continue with the checksum calculation of the other
   // components.
-  if (schema_checksum == Crc32() && !has_schema_successfully_set_) {
+  if (schema_checksum == Crc32() &&
+      absl_ports::IsNotFound(schema_file_.Read().status())) {
     return schema_checksum;
   }
   Crc32 total_checksum;
@@ -875,7 +885,8 @@
   bool ignore_errors_and_delete_documents =
       set_schema_request.ignore_errors_and_delete_documents();
 
-  if (feature_flags_->enable_schema_database()) {
+  if (feature_flags_->enable_schema_database() &&
+      !set_schema_request.database().empty()) {
     // Step 1: (Only required if schema database is enabled)
     // Do some preliminary checks on the new schema before formal validation and
     // delta computation. This checks that:
@@ -946,11 +957,19 @@
   // Get the full new SchemaProto that is a combination of the existing schema
   // and new_schema. This is needed as we can only write the full proto to the
   // schema file.
-  ICING_ASSIGN_OR_RETURN(
-      SchemaProto full_new_schema,
-      GetFullSchemaProtoWithUpdatedDb(std::move(new_schema), database));
+  SchemaProto full_new_schema;
+  if (feature_flags_->enable_schema_type_id_optimization()) {
+    ICING_ASSIGN_OR_RETURN(
+        full_new_schema,
+        GetFullOptimizedSchemaProto(std::move(new_schema), database));
+  } else {
+    ICING_ASSIGN_OR_RETURN(
+        full_new_schema,
+        GetFullSchemaProtoWithUpdatedDb(std::move(new_schema), database));
+  }
   ICING_RETURN_IF_ERROR(ApplySchemaChange(std::move(full_new_schema)));
   has_schema_successfully_set_ = true;
+  ResetSchemaFileIfNeeded();
 
   return result;
 }
@@ -963,30 +982,32 @@
   SetSchemaResult result;
   result.success = true;
 
-  if (feature_flags_->enable_schema_database()) {
-    // Sanity check to make sure that we're comparing schemas from the same
-    // database.
-    // The new code path ensures that old_schema contains types from exactly one
-    // database since it's obtained using GetSchema(database), which is
-    // guaranteed to only return types from the single provided database.
-    libtextclassifier3::Status validate_old_schema_database =
-        ValidateSchemaDatabase(old_schema, database);
-    if (!validate_old_schema_database.ok()) {
-      return absl_ports::InvalidArgumentError(
-          "Schema database mismatch between new and old schemas. This should "
-          "never happen");
-    }
+  if (!feature_flags_->enable_skip_set_schema_type_equality_check()) {
+    if (feature_flags_->enable_schema_database()) {
+      // Sanity check to make sure that we're comparing schemas from the same
+      // database.
+      // The new code path ensures that old_schema contains types from exactly
+      // one database since it's obtained using GetSchema(database), which is
+      // guaranteed to only return types from the single provided database.
+      libtextclassifier3::Status validate_old_schema_database =
+          ValidateSchemaDatabase(old_schema, database);
+      if (!validate_old_schema_database.ok()) {
+        return absl_ports::InvalidArgumentError(
+            "Schema database mismatch between new and old schemas. This should "
+            "never happen");
+      }
 
-    // Check if the schema types are the same between the new and old schema,
-    // ignoring order.
-    if (AreSchemaTypesEqual(new_schema, old_schema)) {
-      return result;
-    }
-  } else {
-    // Old equality check that is sensitive to type definition order.
-    if (new_schema.SerializeAsString() == old_schema.SerializeAsString()) {
-      // Same schema as before. No need to update anything
-      return result;
+      // Check if the schema types are the same between the new and old schema,
+      // ignoring order.
+      if (AreSchemaTypesEqual(new_schema, old_schema)) {
+        return result;
+      }
+    } else {
+      // Old equality check that is sensitive to type definition order.
+      if (new_schema.SerializeAsString() == old_schema.SerializeAsString()) {
+        // Same schema as before. No need to update anything
+        return result;
+      }
     }
   }
 
@@ -1038,9 +1059,16 @@
   // Get the full new SchemaProto that is a combination of the existing schema
   // and new_schema. This is needed to calculate the updated SchemaTypeIds, and
   // for writing the full proto to the schema file.
-  ICING_ASSIGN_OR_RETURN(
-      SchemaProto full_new_schema,
-      GetFullSchemaProtoWithUpdatedDb(std::move(new_schema), database));
+  SchemaProto full_new_schema;
+  if (feature_flags_->enable_schema_type_id_optimization()) {
+    ICING_ASSIGN_OR_RETURN(
+        full_new_schema,
+        GetFullOptimizedSchemaProto(std::move(new_schema), database));
+  } else {
+    ICING_ASSIGN_OR_RETURN(
+        full_new_schema,
+        GetFullSchemaProtoWithUpdatedDb(std::move(new_schema), database));
+  }
 
   // We still need to update old_schema_type_ids_changed. We need to retrieve
   // the entire old schema for this, as type ids are assigned for the entire
@@ -1059,6 +1087,7 @@
   if (result.success) {
     ICING_RETURN_IF_ERROR(ApplySchemaChange(std::move(full_new_schema)));
     has_schema_successfully_set_ = true;
+    ResetSchemaFileIfNeeded();
   }
 
   // Convert schema types to SchemaTypeIds after the new schema is applied.
@@ -1111,6 +1140,12 @@
       SchemaStore::Create(filesystem_, temp_schema_store_dir.dir(), clock_,
                           feature_flags_, std::move(new_schema)));
 
+  // Call PersistToDisk() to write the new schema file to disk. This is needed
+  // to ensure that all subcomponents of the new schema store (header file,
+  // derived files, etc) are written to disk before we swap the files in the
+  // next step.
+  ICING_RETURN_IF_ERROR(new_schema_store->PersistToDisk());
+
   // Then we swap the new schema file + new derived files with the old files.
   if (!filesystem_->SwapFiles(base_dir_.c_str(),
                               temp_schema_store_dir.dir().c_str())) {
@@ -1121,12 +1156,13 @@
   std::string old_base_dir = std::move(base_dir_);
   *this = std::move(*new_schema_store);
 
-  // After the std::move, the filepaths saved in this instance and in the
-  // schema_file_ instance will still be the one from temp_schema_store_dir
-  // even though they now point to files that are within old_base_dir.
-  // Manually set them to the correct paths.
+  // After the std::move, the filepaths saved in this instance, the header_ and
+  // in the schema_file_ instance will still be the one from
+  // temp_schema_store_dir even though they now point to files that are within
+  // old_base_dir. Manually set them to the correct paths.
   base_dir_ = std::move(old_base_dir);
   schema_file_.SetSwappedFilepath(MakeSchemaFilename(base_dir_));
+  header_->SetSwappedFilepath(MakeHeaderFilename(base_dir_));
   if (overlay_schema_file_ != nullptr) {
     overlay_schema_file_->SetSwappedFilepath(
         MakeOverlaySchemaFilename(base_dir_));
@@ -1417,7 +1453,8 @@
 
 libtextclassifier3::Status SchemaStore::ValidateSchemaDatabase(
     const SchemaProto& new_schema, const std::string& database) const {
-  if (!feature_flags_->enable_schema_database() || new_schema.types().empty()) {
+  if (!feature_flags_->enable_schema_database() || new_schema.types().empty() ||
+      database.empty()) {
     return libtextclassifier3::Status::OK;
   }
 
@@ -1443,9 +1480,11 @@
       auto iter = type_config_map_.find(type_config.schema_type());
       if (iter != type_config_map_.end() &&
           database != iter->second.database()) {
-        return absl_ports::AlreadyExistsError(
-            absl_ports::StrCat("schema_type name: '", type_config.schema_type(),
-                               "' is already in use by a different database."));
+        return absl_ports::AlreadyExistsError(absl_ports::StrCat(
+            "schema_type name: '", type_config.schema_type(),
+            "' is already in use by a different database. Request database: '",
+            database, "' vs. existing database: '", iter->second.database(),
+            "'."));
       }
     }
   }
@@ -1453,12 +1492,143 @@
 }
 
 libtextclassifier3::StatusOr<SchemaProto>
+SchemaStore::GetFullOptimizedSchemaProto(
+    SchemaProto input_database_schema,
+    const std::string& database_to_update) const {
+  libtextclassifier3::StatusOr<const SchemaProto*> schema_proto = GetSchema();
+  if (absl_ports::IsNotFound(schema_proto.status())) {
+    // We don't have a pre-existing schema -- we can return the input database
+    // schema as it's already the full schema.
+    return input_database_schema;
+  }
+
+  if (!schema_proto.ok()) {
+    // Real error.
+    return schema_proto.status();
+  }
+
+  if (!has_schema_successfully_set_) {
+    return absl_ports::InternalError(
+        "Schema store was not initialized properly.");
+  }
+
+  // At this point, we have a pre-existing schema -- we need to merge the
+  // updated database with the existing schema.
+  const SchemaProto* existing_schema = schema_proto.ValueOrDie();
+
+  // Step 1: Do some pre-processing to build maps of existing and input
+  // types, as well as a list of newly added types.
+  std::unordered_set<std::string> existing_type_names;
+  for (const SchemaTypeConfigProto& type_config : existing_schema->types()) {
+    if (database_to_update.empty() ||
+        type_config.database() == database_to_update) {
+      existing_type_names.insert(type_config.schema_type());
+    }
+  }
+  std::unordered_map<std::string, SchemaTypeConfigProto*> input_type_map;
+  std::vector<SchemaTypeConfigProto*> new_input_types;
+  for (SchemaTypeConfigProto& type_config :
+       *input_database_schema.mutable_types()) {
+    input_type_map.insert({type_config.schema_type(), &type_config});
+    if (existing_type_names.find(type_config.schema_type()) ==
+        existing_type_names.end()) {
+      new_input_types.push_back(&type_config);
+    }
+  }
+
+  // Step 2: Iterate through existing schema and add them to the full schema,
+  // replacing existing types with the input types if the database is the one
+  // being updated by the input schema.
+  std::vector<SchemaTypeConfigProto> full_schema_types;
+  full_schema_types.reserve(existing_schema->types().size() +
+                            new_input_types.size());
+  std::vector<int> unfilled_type_ids;
+  int new_input_types_index = 0;
+
+  for (int i = 0; i < existing_schema->types().size(); ++i) {
+    const SchemaTypeConfigProto& existing_type_config =
+        existing_schema->types().at(i);
+
+    if (database_to_update.empty() ||
+        existing_type_config.database() == database_to_update) {
+      auto itr = input_type_map.find(existing_type_config.schema_type());
+      if (itr != input_type_map.end()) {
+        // Existing type is still present in the input types. Add the input
+        // type to the full schema at the same position.
+        full_schema_types.push_back(std::move(*itr->second));
+      } else {
+        // Existing type is no longer present in input types. Add a new type
+        // to replace the existing type if the new types are not exhausted.
+        // This is to try to preserve the type-ids of subsequent existing
+        // types that are still present.
+        if (new_input_types_index < new_input_types.size()) {
+          full_schema_types.push_back(
+              std::move(*new_input_types[new_input_types_index++]));
+        } else {
+          // No more input types to replace the existing type. Add a dummy
+          // type instead. This will be replaced with valid type from the end
+          // of the schema later.
+          full_schema_types.push_back(SchemaTypeConfigProto());
+          unfilled_type_ids.push_back(i);
+        }
+      }
+    } else {
+      // Existing type is from a different database. Add it to the full schema
+      // without any changes.
+      full_schema_types.push_back(existing_type_config);
+    }
+  }
+
+  // Step 3a: If there are remaining new input types, append them to the end
+  // of the SchemaProto. This happens when there are more input types than
+  // existing types for database_to_update.
+  for (; new_input_types_index < new_input_types.size();
+       ++new_input_types_index) {
+    full_schema_types.push_back(
+        std::move(*new_input_types[new_input_types_index]));
+  }
+
+  // Step 3b: Iterate from the end of the full schema types and fill the
+  // unfilled type ids with the last few types in the schema. This happens
+  // when there are fewer input types than existing types for
+  // database_to_update.
+  if (!unfilled_type_ids.empty()) {
+    int num_unfilled_types = static_cast<int>(unfilled_type_ids.size());
+
+    // Find non-dummy types at the end of the schema.
+    int unfilled_index = 0;
+    for (int i = 0; i < num_unfilled_types; ++i) {
+      int idx =
+          static_cast<int>(full_schema_types.size()) - num_unfilled_types + i;
+      if (!full_schema_types[idx].schema_type().empty()) {
+        // Backfill idx to unfilled_type_ids[unfilled_index].
+        full_schema_types[unfilled_type_ids[unfilled_index++]] =
+            std::move(full_schema_types[idx]);
+      }
+    }
+
+    // Resize the vector to remove the types that were moved.
+    full_schema_types.resize(full_schema_types.size() - num_unfilled_types);
+  }
+
+  SchemaProto full_schema;
+  full_schema.mutable_types()->Reserve(
+      static_cast<int>(full_schema_types.size()));
+  for (SchemaTypeConfigProto& type_config : full_schema_types) {
+    *full_schema.add_types() = std::move(type_config);
+  }
+
+  return full_schema;
+}
+
+libtextclassifier3::StatusOr<SchemaProto>
 SchemaStore::GetFullSchemaProtoWithUpdatedDb(
     SchemaProto input_database_schema,
     const std::string& database_to_update) const {
-  if (!feature_flags_->enable_schema_database()) {
-    // If the schema database is not enabled, the input schema is already the
-    // full schema, so we don't need to do any merges.
+  if (!feature_flags_->enable_schema_database() || database_to_update.empty()) {
+    // The schema database is not enabled, or we're updating using the empty
+    // schema database. This means that the input schema is already the full
+    // schema, so we don't need to do any merges.
     return input_database_schema;
   }
 
@@ -1491,20 +1661,9 @@
   const SchemaProto* existing_schema = schema_proto.ValueOrDie();
   SchemaProto full_schema;
 
-  // 1. Add types from the existing schema, replacing existing types with the
-  // input types if the database is the one being updated by the input schema.
-  // - For database_to_update, we replace the existing types with the input
-  //   types. Any existing type not included in input_database_schema is
-  //   deleted.
-  // - If there are more input types than existing types for database_to_update,
-  //   the rest of the input types are appended to the end of the full_schema.
-  // - If there are fewer input types than existing types for
-  //   database_to_update, we shift forward all existing types that appear after
-  //   the last input type.
-  // - For existing types from other databases, we preserve the existing order
-  //   after adding to full_schema. Note that the type-ids of existing types
-  //   might still change if some types are deleted in the database_to_update as
-  //   this will cause all subsequent types ids to shift forward.
+  // This old rewrite does not preserve the type-ids of existing types, but
+  // rather inserts the input types for the database in the order in which
+  // they appear in the input proto.
   int input_schema_index = 0, existing_schema_index = 0;
   while (input_schema_index < input_database_schema.types().size() &&
          existing_schema_index < existing_schema->types().size()) {
diff --git a/icing/schema/schema-store.h b/icing/schema/schema-store.h
index b5d89ce..766f52b 100644
--- a/icing/schema/schema-store.h
+++ b/icing/schema/schema-store.h
@@ -30,6 +30,7 @@
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/canonical_errors.h"
+#include "icing/absl_ports/str_cat.h"
 #include "icing/feature-flags.h"
 #include "icing/file/file-backed-proto.h"
 #include "icing/file/filesystem.h"
@@ -157,6 +158,8 @@
           min_overlay_version_compatibility;
     }
 
+    void SetSwappedFilepath(std::string path) { path_ = std::move(path); }
+
    private:
     explicit Header(SerializedHeader serialized_header, std::string path,
                     ScopedFd header_fd, const Filesystem* filesystem)
@@ -253,8 +256,6 @@
 
   static constexpr std::string_view kSchemaTypeWildcard = "*";
 
-  static constexpr std::string_view kDefaultEmptySchemaDatabase = "";
-
   // Factory function to create a SchemaStore which does not take ownership
   // of any input components, and all pointers must refer to valid objects that
   // outlive the created SchemaStore instance. The base_dir must already exist.
@@ -329,18 +330,22 @@
   // `SetSchema(SetSchemaRequestProto&& set_schema_request)` instead.
   //
   // TODO: b/337913932 - Remove this method once all callers (currently only
-  // used in tests) are migrated to the new SetSchema method.
+  // used in tests) are migrated to the new SetSchema method that takes a
+  // SetSchemaRequestProto.
   libtextclassifier3::StatusOr<SetSchemaResult> SetSchema(
       SchemaProto new_schema, bool ignore_errors_and_delete_documents);
 
   // Update our current schema if it's compatible. Does not accept incompatible
-  // schema or schema with types from multiple databases. Compatibility rules
-  // defined by SchemaUtil::ComputeCompatibilityDelta.
+  // schema or schema subsets with types from multiple databases. Compatibility
+  // rules defined by SchemaUtil::ComputeCompatibilityDelta.
   //
-  // Does not support setting the schema across multiple databases if
-  // `feature_flags_->enable_schema_database()` is true. This means that:
-  // - All types within the new schema must have their `database` field matching
-  //  `set_schema_request.database()`.
+  // This method accepts either a full schema (indicated by an empty database
+  // field) or a schema subset with types from a single database.
+  // - If `set_schema_request.database()` is non-empty, then all types in the
+  //   new schema must have their `database` field matching
+  //   `set_schema_request.database()`.
+  // - If `set_schema_request.database()` is empty, then the new schema will be
+  //   taken as the full schema, and will replace the entire existing schema.
   //
   // If ignore_errors_and_delete_documents is set to true, then incompatible
   // schema are allowed and we'll force set the schema, meaning
@@ -578,6 +583,26 @@
       const google::protobuf::RepeatedPtrField<TypePropertyMask>& type_property_masks)
       const;
 
+  // Returns the hash of a schema name.
+  static uint32_t GetSchemaNameHash(std::string_view schema_name) {
+    return Crc32(schema_name).Get();
+  }
+
+  // Returns the hash of the schema name for the given schema type id.
+  //
+  // Returns:
+  //   - The hash value on success.
+  //   - INVALID_ARGUMENT_ERROR if schema_type_id is invalid.
+  libtextclassifier3::StatusOr<uint32_t> GetSchemaNameHash(
+      SchemaTypeId schema_type_id) const {
+    auto it = reverse_schema_type_mapper_hash_.find(schema_type_id);
+    if (it == reverse_schema_type_mapper_hash_.end()) {
+      return absl_ports::InvalidArgumentError(absl_ports::StrCat(
+          "Invalid SchemaTypeId ", std::to_string(schema_type_id)));
+    }
+    return it->second;
+  }
+
  private:
   // Factory function to create a SchemaStore and set its schema. The created
   // instance does not take ownership of any input components and all pointers
@@ -763,6 +788,7 @@
   // Requires:
   //   - `new_schema` and `database` are valid according to
   //     `ValidateSchemaDatabase(new_schema, database)`
+  //   - `database` is not empty.
   //   - Types in `new_schema` and `old_schema` all belong to the provided
   //     database.
   //     - The old schema is guaranteed to contain types from exactly one
@@ -805,6 +831,59 @@
       const SchemaProto& new_schema, const std::string& database) const;
 
   // Returns a SchemaProto representing the full schema, which is a combination
+  // of the existing schema and the input database schema. The returned
+  // SchemaProto is optimized to preserve as many type ids as possible.
+  //
+  // Note that `database_to_update` could also be the empty string, which means
+  // that the entire schema is being updated. In this case,
+  // `input_database_schema` must contain all types in the full schema. Any
+  // preexisting type not in `input_database_schema` will be deleted.
+  //
+  // For database_to_update, we replace the existing types with the input
+  //   types. Any existing type not included in input_database_schema is
+  //   deleted.
+  // - When possible, existing types are added in the position in which they
+  //   appear in the existing schema so as to preserve the type-ids of
+  //   existing types.
+  // - If there are more input types than existing types for
+  //   database_to_update, added input types are appended to the end of the
+  //   full_schema.
+  // - If there are fewer input types than existing types for
+  //   database_to_update, we use the last few input types to replace the
+  //   deleted existing types, so as to preserve as many old type-ids as
+  //   possible.
+  // - For existing types from other databases, we preserve the existing order
+  //   after adding to full_schema. Note that the type-ids of existing types
+  //   might still change if some types are deleted in the database_to_update
+  //   as this will cause all subsequent types ids to shift forward.
+  // - This means that:
+  //   - When adding types to a database, the type-ids of existing types will
+  //     not change.
+  //   - When types are deleted, we fill their original slots with the last
+  //     valid types in the schema to preserve as many type-ids as possible.
+  // - If input_database_schema is an empty proto, then all types from
+  //   database_to_update are deleted.
+  //
+  // Requires:
+  //   - input_database_schema is valid according to `ValidateSchemaDatabase`.
+  //
+  // Returns:
+  //   - SchemaProto on success
+  //   - INTERNAL_ERROR on any IO errors, or if the schema store was not
+  //     previously initialized properly.
+  //   - INVALID_ARGUMENT_ERROR if the input schema does not match
+  //     database_to_update.
+  libtextclassifier3::StatusOr<SchemaProto> GetFullOptimizedSchemaProto(
+      SchemaProto input_database_schema,
+      const std::string& database_to_update) const;
+
+  // TODO: b/434218554 - Remove this method once schema type id optimization is
+  // fully rolled out.
+  //
+  // This method should only be called when
+  // `feature_flags_->enable_schema_type_id_optimization` is false.
+  //
+  // Returns a SchemaProto representing the full schema, which is a combination
   // of the existing schema and the input database schema. Deletes all types
   // belonging to the specified database if input_database_schema is an empty
   // proto.
@@ -934,6 +1013,12 @@
   // map of schema_type_mapper_.
   std::unordered_map<SchemaTypeId, std::string> reverse_schema_type_mapper_;
 
+  // Maps schema type ids to the hash value of the corresponding schema type
+  // name.
+  // TODO(b/436237337): Consider merging this with reverse_schema_type_mapper_
+  // to save memory.
+  std::unordered_map<SchemaTypeId, uint32_t> reverse_schema_type_mapper_hash_;
+
   // A hash map of (database -> vector of type config names in the database).
   //
   // We use a vector instead of a set because we need to preserve the order of
diff --git a/icing/schema/schema-store_test.cc b/icing/schema/schema-store_test.cc
index 4993408..8afe4a1 100644
--- a/icing/schema/schema-store_test.cc
+++ b/icing/schema/schema-store_test.cc
@@ -14,6 +14,7 @@
 
 #include "icing/schema/schema-store.h"
 
+#include <algorithm>
 #include <cstdint>
 #include <memory>
 #include <optional>
@@ -620,65 +621,66 @@
                           feature_flags_.get(),
                           /*initialize_stats=*/nullptr));
 
-  SchemaProto db1_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .Build();
+  SchemaProto db1_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/email")
+                                            .SetDatabase("db1/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/message")
+                                            .SetDatabase("db1/"))
+                               .Build();
   SchemaStore::SetSchemaResult result;
   result.success = true;
-  result.schema_types_new_by_name.insert("db1_email");
-  result.schema_types_new_by_name.insert("db1_message");
+  result.schema_types_new_by_name.insert("db1/email");
+  result.schema_types_new_by_name.insert("db1/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db1_schema, /*database=*/"db1",
+                  db1_schema, /*database=*/"db1/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   EXPECT_THAT(schema_store->GetSchema(),
               IsOkAndHolds(Pointee(EqualsProto(db1_schema))));
-  EXPECT_THAT(schema_store->GetSchema("db1"),
+  EXPECT_THAT(schema_store->GetSchema("db1/"),
               IsOkAndHolds(EqualsProto(db1_schema)));
 
   // Set a schema in a different database
-  SchemaProto db2_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .Build();
+  SchemaProto db2_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/email")
+                                            .SetDatabase("db2/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/message")
+                                            .SetDatabase("db2/"))
+                               .Build();
   result = SchemaStore::SetSchemaResult();
   result.success = true;
-  result.schema_types_new_by_name.insert("db2_email");
-  result.schema_types_new_by_name.insert("db2_message");
+  result.schema_types_new_by_name.insert("db2/email");
+  result.schema_types_new_by_name.insert("db2/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db2_schema, /*database=*/"db2",
+                  db2_schema, /*database=*/"db2/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
 
   // Check the full schema. Databases that are updated last are appended to the
   // schema proto
-  SchemaProto expected_full_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .Build();
+  SchemaProto expected_full_schema = SchemaBuilder()
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db1/email")
+                                                      .SetDatabase("db1/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db1/message")
+                                                      .SetDatabase("db1/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/email")
+                                                      .SetDatabase("db2/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/message")
+                                                      .SetDatabase("db2/"))
+                                         .Build();
   EXPECT_THAT(schema_store->GetSchema(),
               IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
-  EXPECT_THAT(schema_store->GetSchema("db1"),
+  EXPECT_THAT(schema_store->GetSchema("db1/"),
               IsOkAndHolds(EqualsProto(db1_schema)));
-  EXPECT_THAT(schema_store->GetSchema("db2"),
+  EXPECT_THAT(schema_store->GetSchema("db2/"),
               IsOkAndHolds(EqualsProto(db2_schema)));
 }
 
@@ -732,6 +734,175 @@
   EXPECT_THAT(schema_store->GetSchema(""), IsOkAndHolds(EqualsProto(schema)));
 }
 
+TEST_F(SchemaStoreTest, SetSchemaWithEmptyDatabaseResetsEntireSchema) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
+
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("email"))
+          .AddType(SchemaTypeConfigBuilder().SetType("message"))
+          .Build();
+  SchemaStore::SetSchemaResult result;
+  result.success = true;
+  result.schema_types_new_by_name.insert("email");
+  result.schema_types_new_by_name.insert("message");
+  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                  schema, /*database=*/"",
+                  /*ignore_errors_and_delete_documents=*/false)),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+  EXPECT_THAT(schema_store->GetSchema(),
+              IsOkAndHolds(Pointee(EqualsProto(schema))));
+  EXPECT_THAT(schema_store->GetSchema(""), IsOkAndHolds(EqualsProto(schema)));
+
+  // Reset the schema using an empty database. This should ignore the
+  // database field in the schema type configs and reset the entire schema.
+  schema = SchemaBuilder()
+               .AddType(SchemaTypeConfigBuilder()
+                            .SetType("db1/email_v1")
+                            .SetDatabase("db1/"))
+               .AddType(SchemaTypeConfigBuilder()
+                            .SetType("db1/message_v1")
+                            .SetDatabase("db1/"))
+               .AddType(SchemaTypeConfigBuilder()
+                            .SetType("db2/email_v2")
+                            .SetDatabase("db2/"))
+               .AddType(SchemaTypeConfigBuilder()
+                            .SetType("db2/message_v2")
+                            .SetDatabase("db2/"))
+               .Build();
+  result = SchemaStore::SetSchemaResult();
+  result.success = true;
+  result.schema_types_new_by_name.insert("db1/email_v1");
+  result.schema_types_new_by_name.insert("db1/message_v1");
+  result.schema_types_new_by_name.insert("db2/email_v2");
+  result.schema_types_new_by_name.insert("db2/message_v2");
+  result.schema_types_deleted_by_name.insert("email");
+  result.schema_types_deleted_by_name.insert("message");
+  result.schema_types_deleted_by_id.insert(0);
+  result.schema_types_deleted_by_id.insert(1);
+
+  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                  schema, /*database=*/"",
+                  /*ignore_errors_and_delete_documents=*/true)),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+  EXPECT_THAT(schema_store->GetSchema(),
+              IsOkAndHolds(Pointee(EqualsProto(schema))));
+
+  // The empty database should be replaced by db1/ and db2/.
+  EXPECT_THAT(schema_store->GetSchema(""),
+              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
+  EXPECT_THAT(
+      schema_store->GetSchema("db1/"),
+      IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db1/email_v1")
+                                                .SetDatabase("db1/"))
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db1/message_v1")
+                                                .SetDatabase("db1/"))
+                                   .Build())));
+  EXPECT_THAT(
+      schema_store->GetSchema("db2/"),
+      IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db2/email_v2")
+                                                .SetDatabase("db2/"))
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db2/message_v2")
+                                                .SetDatabase("db2/"))
+                                   .Build())));
+}
+
+TEST_F(SchemaStoreTest, SetSchemaWithUnpopulatedDatabaseResetsEntireSchema) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
+
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("email"))
+          .AddType(SchemaTypeConfigBuilder().SetType("message"))
+          .Build();
+  SchemaStore::SetSchemaResult result;
+  result.success = true;
+  result.schema_types_new_by_name.insert("email");
+  result.schema_types_new_by_name.insert("message");
+  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                  schema, /*database=*/"",
+                  /*ignore_errors_and_delete_documents=*/false)),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+  EXPECT_THAT(schema_store->GetSchema(),
+              IsOkAndHolds(Pointee(EqualsProto(schema))));
+  EXPECT_THAT(schema_store->GetSchema(""), IsOkAndHolds(EqualsProto(schema)));
+
+  schema = SchemaBuilder()
+               .AddType(SchemaTypeConfigBuilder()
+                            .SetType("db1/email_v1")
+                            .SetDatabase("db1/"))
+               .AddType(SchemaTypeConfigBuilder()
+                            .SetType("db1/message_v1")
+                            .SetDatabase("db1/"))
+               .AddType(SchemaTypeConfigBuilder()
+                            .SetType("db2/email_v2")
+                            .SetDatabase("db2/"))
+               .AddType(SchemaTypeConfigBuilder()
+                            .SetType("db2/message_v2")
+                            .SetDatabase("db2/"))
+               .Build();
+  result = SchemaStore::SetSchemaResult();
+  result.success = true;
+  result.schema_types_new_by_name.insert("db1/email_v1");
+  result.schema_types_new_by_name.insert("db1/message_v1");
+  result.schema_types_new_by_name.insert("db2/email_v2");
+  result.schema_types_new_by_name.insert("db2/message_v2");
+  result.schema_types_deleted_by_name.insert("email");
+  result.schema_types_deleted_by_name.insert("message");
+  result.schema_types_deleted_by_id.insert(0);
+  result.schema_types_deleted_by_id.insert(1);
+
+  // Reset the schema without populating the database field in the
+  // SetSchemaRequestProto. This should ignore the database field in the schema
+  // type configs and reset the entire schema.
+  SetSchemaRequestProto set_schema_request;
+  *set_schema_request.mutable_schema() = schema;
+  set_schema_request.clear_database();
+  set_schema_request.set_ignore_errors_and_delete_documents(
+      /*ignore_errors_and_delete_documents=*/true);
+
+  EXPECT_THAT(schema_store->SetSchema(std::move(set_schema_request)),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+  EXPECT_THAT(schema_store->GetSchema(),
+              IsOkAndHolds(Pointee(EqualsProto(schema))));
+
+  // The empty database should be replaced by db1/ and db2/.
+  EXPECT_THAT(schema_store->GetSchema(""),
+              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
+  EXPECT_THAT(
+      schema_store->GetSchema("db1/"),
+      IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db1/email_v1")
+                                                .SetDatabase("db1/"))
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db1/message_v1")
+                                                .SetDatabase("db1/"))
+                                   .Build())));
+  EXPECT_THAT(
+      schema_store->GetSchema("db2/"),
+      IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db2/email_v2")
+                                                .SetDatabase("db2/"))
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db2/message_v2")
+                                                .SetDatabase("db2/"))
+                                   .Build())));
+}
+
 TEST_F(SchemaStoreTest, SetSameSchemaOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
@@ -767,49 +938,50 @@
                           /*initialize_stats=*/nullptr));
 
   // Set schema for the first time
-  SchemaProto db1_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .Build();
-  SchemaProto db2_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .Build();
-  SchemaProto expected_full_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .Build();
+  SchemaProto db1_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/email")
+                                            .SetDatabase("db1/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/message")
+                                            .SetDatabase("db1/"))
+                               .Build();
+  SchemaProto db2_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/email")
+                                            .SetDatabase("db2/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/message")
+                                            .SetDatabase("db2/"))
+                               .Build();
+  SchemaProto expected_full_schema = SchemaBuilder()
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db1/email")
+                                                      .SetDatabase("db1/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db1/message")
+                                                      .SetDatabase("db1/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/email")
+                                                      .SetDatabase("db2/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/message")
+                                                      .SetDatabase("db2/"))
+                                         .Build();
   SchemaStore::SetSchemaResult result;
   result.success = true;
-  result.schema_types_new_by_name.insert("db1_email");
-  result.schema_types_new_by_name.insert("db1_message");
+  result.schema_types_new_by_name.insert("db1/email");
+  result.schema_types_new_by_name.insert("db1/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db1_schema, /*database=*/"db1",
+                  db1_schema, /*database=*/"db1/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   result = SchemaStore::SetSchemaResult();
   result.success = true;
-  result.schema_types_new_by_name.insert("db2_email");
-  result.schema_types_new_by_name.insert("db2_message");
+  result.schema_types_new_by_name.insert("db2/email");
+  result.schema_types_new_by_name.insert("db2/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db2_schema, /*database=*/"db2",
+                  db2_schema, /*database=*/"db2/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   ICING_ASSERT_OK_AND_ASSIGN(const SchemaProto* actual_full_schema,
@@ -820,16 +992,16 @@
   result = SchemaStore::SetSchemaResult();
   result.success = true;
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db1_schema, /*database=*/"db1",
+                  db1_schema, /*database=*/"db1/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
 
   // Check the schema, this should not have changed
   EXPECT_THAT(schema_store->GetSchema(),
               IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
-  EXPECT_THAT(schema_store->GetSchema("db1"),
+  EXPECT_THAT(schema_store->GetSchema("db1/"),
               IsOkAndHolds(EqualsProto(db1_schema)));
-  EXPECT_THAT(schema_store->GetSchema("db2"),
+  EXPECT_THAT(schema_store->GetSchema("db2/"),
               IsOkAndHolds(EqualsProto(db2_schema)));
 }
 
@@ -841,74 +1013,76 @@
                           /*initialize_stats=*/nullptr));
 
   // Set schema for the first time
-  SchemaProto db1_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .Build();
-  SchemaProto db2_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .Build();
-  SchemaProto db3_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db3_email").SetDatabase("db3"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db3_message")
-                       .SetDatabase("db3"))
-          .Build();
-  SchemaProto expected_full_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db3_email").SetDatabase("db3"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db3_message")
-                       .SetDatabase("db3"))
-          .Build();
+  SchemaProto db1_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/email")
+                                            .SetDatabase("db1/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/message")
+                                            .SetDatabase("db1/"))
+                               .Build();
+  SchemaProto db2_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/email")
+                                            .SetDatabase("db2/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/message")
+                                            .SetDatabase("db2/"))
+                               .Build();
+  SchemaProto db3_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db3/email")
+                                            .SetDatabase("db3/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db3/message")
+                                            .SetDatabase("db3/"))
+                               .Build();
+  SchemaProto expected_full_schema = SchemaBuilder()
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db1/email")
+                                                      .SetDatabase("db1/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db1/message")
+                                                      .SetDatabase("db1/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/email")
+                                                      .SetDatabase("db2/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/message")
+                                                      .SetDatabase("db2/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db3/email")
+                                                      .SetDatabase("db3/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db3/message")
+                                                      .SetDatabase("db3/"))
+                                         .Build();
 
   // Set schema for db1
   SchemaStore::SetSchemaResult result;
   result.success = true;
-  result.schema_types_new_by_name.insert("db1_email");
-  result.schema_types_new_by_name.insert("db1_message");
+  result.schema_types_new_by_name.insert("db1/email");
+  result.schema_types_new_by_name.insert("db1/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db1_schema, /*database=*/"db1",
+                  db1_schema, /*database=*/"db1/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   // Set schema for db2
   result = SchemaStore::SetSchemaResult();
   result.success = true;
-  result.schema_types_new_by_name.insert("db2_email");
-  result.schema_types_new_by_name.insert("db2_message");
+  result.schema_types_new_by_name.insert("db2/email");
+  result.schema_types_new_by_name.insert("db2/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db2_schema, /*database=*/"db2",
+                  db2_schema, /*database=*/"db2/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   // Set schema for db3
   result = SchemaStore::SetSchemaResult();
   result.success = true;
-  result.schema_types_new_by_name.insert("db3_email");
-  result.schema_types_new_by_name.insert("db3_message");
+  result.schema_types_new_by_name.insert("db3/email");
+  result.schema_types_new_by_name.insert("db3/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db3_schema, /*database=*/"db3",
+                  db3_schema, /*database=*/"db3/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   // Verify schema.
@@ -918,20 +1092,20 @@
 
   // Reset db2 with the types reordered. This should not change the existing
   // schema in any way.
-  SchemaProto reordered_db2_schema =
-      SchemaBuilder()
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .Build();
+  SchemaProto reordered_db2_schema = SchemaBuilder()
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/message")
+                                                      .SetDatabase("db2/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/email")
+                                                      .SetDatabase("db2/"))
+                                         .Build();
   result = SchemaStore::SetSchemaResult();
   result.success = true;
 
   libtextclassifier3::StatusOr<SchemaStore::SetSchemaResult> actual_result =
       schema_store->SetSchema(CreateSetSchemaRequestProto(
-          reordered_db2_schema, /*database=*/"db2",
+          reordered_db2_schema, /*database=*/"db2/",
           /*ignore_errors_and_delete_documents=*/false));
   EXPECT_THAT(actual_result, IsOkAndHolds(EqualsSetSchemaResult(result)));
   EXPECT_THAT(actual_result.ValueOrDie().old_schema_type_ids_changed,
@@ -940,20 +1114,23 @@
   // Check the schema
   EXPECT_THAT(schema_store->GetSchema(),
               IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
-  EXPECT_THAT(schema_store->GetSchema("db1"),
+  EXPECT_THAT(schema_store->GetSchema("db1/"),
               IsOkAndHolds(EqualsProto(db1_schema)));
 
   libtextclassifier3::StatusOr<SchemaProto> actual_db2_schema =
-      schema_store->GetSchema("db2");
+      schema_store->GetSchema("db2/");
   EXPECT_THAT(actual_db2_schema, IsOkAndHolds(EqualsProto(db2_schema)));
   EXPECT_THAT(actual_db2_schema.ValueOrDie(),
               Not(EqualsProto(reordered_db2_schema)));
 
-  EXPECT_THAT(schema_store->GetSchema("db3"),
+  EXPECT_THAT(schema_store->GetSchema("db3/"),
               IsOkAndHolds(EqualsProto(db3_schema)));
 }
 
-TEST_F(SchemaStoreTest, SetDatabaseAddedTypesPreservesSchemaTypeIds) {
+TEST_F(SchemaStoreTest, SetDatabaseAddedTypesPreserveSchemaTypeIds) {
+  if (!feature_flags_->enable_schema_type_id_optimization()) {
+    GTEST_SKIP() << "Test for schema type id optimization.";
+  }
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
       SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
@@ -961,74 +1138,228 @@
                           /*initialize_stats=*/nullptr));
 
   // Set schema for the first time
-  SchemaProto db1_schema =
+  SchemaProto db1_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/email")
+                                            .SetDatabase("db1/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/message")
+                                            .SetDatabase("db1/"))
+                               .Build();
+  SchemaProto db2_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/email")
+                                            .SetDatabase("db2/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/message")
+                                            .SetDatabase("db2/"))
+                               .Build();
+  SchemaProto db3_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db3/email")
+                                            .SetDatabase("db3/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db3/message")
+                                            .SetDatabase("db3/"))
+                               .Build();
+  SchemaProto expected_full_schema = SchemaBuilder()
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db1/email")
+                                                      .SetDatabase("db1/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db1/message")
+                                                      .SetDatabase("db1/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/email")
+                                                      .SetDatabase("db2/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/message")
+                                                      .SetDatabase("db2/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db3/email")
+                                                      .SetDatabase("db3/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db3/message")
+                                                      .SetDatabase("db3/"))
+                                         .Build();
+
+  // Set schema for db1
+  SchemaStore::SetSchemaResult expected_result;
+  expected_result.success = true;
+  expected_result.schema_types_new_by_name.insert("db1/email");
+  expected_result.schema_types_new_by_name.insert("db1/message");
+  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                  db1_schema, /*database=*/"db1/",
+                  /*ignore_errors_and_delete_documents=*/false)),
+              IsOkAndHolds(EqualsSetSchemaResult(expected_result)));
+  // Set schema for db2
+  expected_result = SchemaStore::SetSchemaResult();
+  expected_result.success = true;
+  expected_result.schema_types_new_by_name.insert("db2/email");
+  expected_result.schema_types_new_by_name.insert("db2/message");
+  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                  db2_schema, /*database=*/"db2/",
+                  /*ignore_errors_and_delete_documents=*/false)),
+              IsOkAndHolds(EqualsSetSchemaResult(expected_result)));
+  // Set schema for db3
+  expected_result = SchemaStore::SetSchemaResult();
+  expected_result.success = true;
+  expected_result.schema_types_new_by_name.insert("db3/email");
+  expected_result.schema_types_new_by_name.insert("db3/message");
+  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                  db3_schema, /*database=*/"db3/",
+                  /*ignore_errors_and_delete_documents=*/false)),
+              IsOkAndHolds(EqualsSetSchemaResult(expected_result)));
+  // Verify schema.
+  ICING_ASSERT_OK_AND_ASSIGN(const SchemaProto* actual_full_schema,
+                             schema_store->GetSchema());
+  EXPECT_THAT(*actual_full_schema, EqualsProto(expected_full_schema));
+
+  // Reset db2 and add a type. The added type should be appended to the end of
+  // the SchemaProto, and SchemaTypeIds for existing types should not change.
+  db2_schema = SchemaBuilder()
+                   .AddType(SchemaTypeConfigBuilder()
+                                .SetType("db2/recipient")
+                                .SetDatabase("db2/"))
+                   .AddType(SchemaTypeConfigBuilder()
+                                .SetType("db2/email")
+                                .SetDatabase("db2/"))
+                   .AddType(SchemaTypeConfigBuilder()
+                                .SetType("db2/message")
+                                .SetDatabase("db2/"))
+                   .Build();
+  expected_full_schema =
       SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
+          .AddType(SchemaTypeConfigBuilder()  // db1 types
+                       .SetType("db1/email")
+                       .SetDatabase("db1/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
+                       .SetType("db1/message")
+                       .SetDatabase("db1/"))
+          .AddType(SchemaTypeConfigBuilder()  // db2 types
+                       .SetType("db2/email")
+                       .SetDatabase("db2/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2/message")
+                       .SetDatabase("db2/"))
+          .AddType(SchemaTypeConfigBuilder()  // db3 types
+                       .SetType("db3/email")
+                       .SetDatabase("db3/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db3/message")
+                       .SetDatabase("db3/"))
+          .AddType(SchemaTypeConfigBuilder()  // Additional db2 type is appended
+                                              // at the end
+                       .SetType("db2/recipient")
+                       .SetDatabase("db2/"))
           .Build();
-  SchemaProto db2_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .Build();
-  SchemaProto db3_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db3_email").SetDatabase("db3"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db3_message")
-                       .SetDatabase("db3"))
-          .Build();
-  SchemaProto expected_full_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db3_email").SetDatabase("db3"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db3_message")
-                       .SetDatabase("db3"))
-          .Build();
+  expected_result = SchemaStore::SetSchemaResult();
+  expected_result.success = true;
+  expected_result.schema_types_new_by_name.insert("db2/recipient");
+  libtextclassifier3::StatusOr<SchemaStore::SetSchemaResult> actual_result =
+      schema_store->SetSchema(CreateSetSchemaRequestProto(
+          db2_schema, /*database=*/"db2/",
+          /*ignore_errors_and_delete_documents=*/false));
+  EXPECT_THAT(actual_result,
+              IsOkAndHolds(EqualsSetSchemaResult(expected_result)));
+  EXPECT_THAT(actual_result.ValueOrDie().old_schema_type_ids_changed,
+              IsEmpty());
+
+  // Check the schema
+  EXPECT_THAT(schema_store->GetSchema(),
+              IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
+  EXPECT_THAT(schema_store->GetSchema("db1/"),
+              IsOkAndHolds(EqualsProto(db1_schema)));
+  EXPECT_THAT(
+      schema_store->GetSchema("db2/"),
+      IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db2/email")
+                                                .SetDatabase("db2/"))
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db2/message")
+                                                .SetDatabase("db2/"))
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db2/recipient")
+                                                .SetDatabase("db2/"))
+                                   .Build())));
+  EXPECT_THAT(schema_store->GetSchema("db3/"),
+              IsOkAndHolds(EqualsProto(db3_schema)));
+}
+
+TEST_F(SchemaStoreTest, SetDatabaseReorderedSchemaPreservesSchemaTypeIds) {
+  if (!feature_flags_->enable_schema_database()) {
+    GTEST_SKIP() << "Test for schema database id assignment.";
+  }
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*initialize_stats=*/nullptr));
+
+  // Set schema for the first time
+  SchemaProto db1_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/email")
+                                            .SetDatabase("db1/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/message")
+                                            .SetDatabase("db1/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/recipient")
+                                            .SetDatabase("db1/"))
+                               .Build();
+  SchemaProto db2_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/email")
+                                            .SetDatabase("db2/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/message")
+                                            .SetDatabase("db2/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/recipient")
+                                            .SetDatabase("db2/"))
+                               .Build();
+  SchemaProto expected_full_schema = SchemaBuilder()
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db1/email")
+                                                      .SetDatabase("db1/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db1/message")
+                                                      .SetDatabase("db1/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db1/recipient")
+                                                      .SetDatabase("db1/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/email")
+                                                      .SetDatabase("db2/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/message")
+                                                      .SetDatabase("db2/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/recipient")
+                                                      .SetDatabase("db2/"))
+                                         .Build();
 
   // Set schema for db1
   SchemaStore::SetSchemaResult result;
   result.success = true;
-  result.schema_types_new_by_name.insert("db1_email");
-  result.schema_types_new_by_name.insert("db1_message");
+  result.schema_types_new_by_name.insert("db1/email");
+  result.schema_types_new_by_name.insert("db1/message");
+  result.schema_types_new_by_name.insert("db1/recipient");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db1_schema, /*database=*/"db1",
+                  db1_schema, /*database=*/"db1/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   // Set schema for db2
   result = SchemaStore::SetSchemaResult();
   result.success = true;
-  result.schema_types_new_by_name.insert("db2_email");
-  result.schema_types_new_by_name.insert("db2_message");
+  result.schema_types_new_by_name.insert("db2/email");
+  result.schema_types_new_by_name.insert("db2/message");
+  result.schema_types_new_by_name.insert("db2/recipient");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db2_schema, /*database=*/"db2",
-                  /*ignore_errors_and_delete_documents=*/false)),
-              IsOkAndHolds(EqualsSetSchemaResult(result)));
-  // Set schema for db3
-  result = SchemaStore::SetSchemaResult();
-  result.success = true;
-  result.schema_types_new_by_name.insert("db3_email");
-  result.schema_types_new_by_name.insert("db3_message");
-  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db3_schema, /*database=*/"db3",
+                  db2_schema, /*database=*/"db2/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   // Verify schema.
@@ -1036,65 +1367,114 @@
                              schema_store->GetSchema());
   EXPECT_THAT(*actual_full_schema, EqualsProto(expected_full_schema));
 
-  // Reset db2 and add a type. The added type should be appended to the end of
-  // the SchemaProto, and SchemaTypeIds for db1 and db3 should not change.
-  //
-  // Whether or not the SchemaTypeIds for db2 change depends on the order in the
-  // new db2 SchemaProto (in this case, existing type's order and ids do not
-  // change)
-  db2_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_recipient")
-                       .SetDatabase("db2"))
-          .Build();
-  expected_full_schema =
-      SchemaBuilder()
-          .AddType(SchemaTypeConfigBuilder()  // db1 types
-                       .SetType("db1_email")
-                       .SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()  // db2 types
-                       .SetType("db2_email")
-                       .SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()  // db3 types
-                       .SetType("db3_email")
-                       .SetDatabase("db3"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db3_message")
-                       .SetDatabase("db3"))
-          .AddType(SchemaTypeConfigBuilder()  // Additional db2 type is appended
-                                              // at the end
-                       .SetType("db2_recipient")
-                       .SetDatabase("db2"))
-          .Build();
+  // Reset db1 with the types reordered. SchemaTypeIds should not change.
+  SchemaProto new_db1_schema = SchemaBuilder()
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db1/email")
+                                                .SetDatabase("db1/"))
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db1/recipient")
+                                                .SetDatabase("db1/"))
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db1/message")
+                                                .SetDatabase("db1/"))
+                                   .Build();
   result = SchemaStore::SetSchemaResult();
   result.success = true;
-  result.schema_types_new_by_name.insert("db2_recipient");
-  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db2_schema, /*database=*/"db2",
-                  /*ignore_errors_and_delete_documents=*/false)),
-              IsOkAndHolds(EqualsSetSchemaResult(result)));
+  libtextclassifier3::StatusOr<SchemaStore::SetSchemaResult> actual_result =
+      schema_store->SetSchema(CreateSetSchemaRequestProto(
+          db2_schema, /*database=*/"db2/",
+          /*ignore_errors_and_delete_documents=*/false));
+  EXPECT_THAT(actual_result, IsOkAndHolds(EqualsSetSchemaResult(result)));
+  EXPECT_THAT(actual_result.ValueOrDie().old_schema_type_ids_changed,
+              IsEmpty());
 
   // Check the schema
   EXPECT_THAT(schema_store->GetSchema(),
               IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
-  EXPECT_THAT(schema_store->GetSchema("db1"),
+  EXPECT_THAT(schema_store->GetSchema("db1/"),
               IsOkAndHolds(EqualsProto(db1_schema)));
-  EXPECT_THAT(schema_store->GetSchema("db2"),
+  EXPECT_THAT(schema_store->GetSchema("db2/"),
               IsOkAndHolds(EqualsProto(db2_schema)));
-  EXPECT_THAT(schema_store->GetSchema("db3"),
-              IsOkAndHolds(EqualsProto(db3_schema)));
+}
+
+TEST_F(SchemaStoreTest,
+       SetDatabaseReorderedSchemaPreservesSchemaTypeIds_largeSchema) {
+  if (!feature_flags_->enable_schema_database()) {
+    GTEST_SKIP() << "Test for schema database id assignment.";
+  }
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*initialize_stats=*/nullptr));
+
+  // 1. Create five databases, each with five types.
+  constexpr int kNumDbs = 5;
+  constexpr int kNumTypesPerDb = 5;
+  SchemaBuilder expected_full_schema_builder;
+
+  for (int i = 0; i < kNumDbs; ++i) {
+    std::string db_name = absl_ports::StrCat("db", std::to_string(i), "/");
+    SchemaBuilder db_schema_builder;
+    SchemaStore::SetSchemaResult expected_result;
+
+    for (int j = 0; j < kNumTypesPerDb; ++j) {
+      std::string type_name =
+          absl_ports::StrCat(db_name, "type", std::to_string(j));
+      db_schema_builder.AddType(
+          SchemaTypeConfigBuilder().SetType(type_name).SetDatabase(db_name));
+      expected_full_schema_builder.AddType(
+          SchemaTypeConfigBuilder().SetType(type_name).SetDatabase(db_name));
+      expected_result.schema_types_new_by_name.insert(type_name);
+    }
+    SchemaProto db_schema = db_schema_builder.Build();
+    expected_result.success = true;
+    EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                    db_schema, db_name,
+                    /*ignore_errors_and_delete_documents=*/false)),
+                IsOkAndHolds(EqualsSetSchemaResult(expected_result)));
+  }
+  SchemaProto expected_full_schema = expected_full_schema_builder.Build();
+
+  // Verify schema.
+  ICING_ASSERT_OK_AND_ASSIGN(const SchemaProto* actual_full_schema,
+                             schema_store->GetSchema());
+  EXPECT_THAT(*actual_full_schema, EqualsProto(expected_full_schema));
+
+  // 2. In a loop, change the ordering of the types in one of the dbs, call
+  // setSchema and verify that type ids remain the same.
+  for (int i = 0; i < kNumDbs; ++i) {
+    std::string db_name = absl_ports::StrCat("db", std::to_string(i), "/");
+    ICING_ASSERT_OK_AND_ASSIGN(SchemaProto db_schema,
+                               schema_store->GetSchema(db_name));
+
+    // Reset the schema with the types reordered.
+    SchemaProto db_schema_reversed = db_schema;
+    std::reverse(db_schema_reversed.mutable_types()->begin(),
+                 db_schema_reversed.mutable_types()->end());
+
+    SchemaStore::SetSchemaResult expected_result =
+        SchemaStore::SetSchemaResult();
+    expected_result.success = true;
+    libtextclassifier3::StatusOr<SchemaStore::SetSchemaResult> actual_result =
+        schema_store->SetSchema(CreateSetSchemaRequestProto(
+            db_schema_reversed, db_name,
+            /*ignore_errors_and_delete_documents=*/false));
+    EXPECT_THAT(actual_result,
+                IsOkAndHolds(EqualsSetSchemaResult(expected_result)));
+
+    // Verify that no type ids changed.
+    EXPECT_THAT(actual_result.ValueOrDie().old_schema_type_ids_changed,
+                IsEmpty());
+
+    // Verify schema
+    EXPECT_THAT(schema_store->GetSchema(db_name),
+                IsOkAndHolds(EqualsProto(db_schema)));
+    EXPECT_THAT(schema_store->GetSchema(),
+                IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
+  }
 }
 
 TEST_F(SchemaStoreTest, SetDatabaseDeletedTypesOk) {
@@ -1105,157 +1485,917 @@
                           /*initialize_stats=*/nullptr));
 
   // Set schema for the first time
-  SchemaProto db1_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .Build();
-  SchemaProto db2_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .Build();
-  SchemaProto db3_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db3_email").SetDatabase("db3"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db3_message")
-                       .SetDatabase("db3"))
-          .Build();
-
+  SchemaProto db1_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/email")
+                                            .SetDatabase("db1/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/message")
+                                            .SetDatabase("db1/"))
+                               .Build();
+  SchemaProto db2_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/email")
+                                            .SetDatabase("db2/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/message")
+                                            .SetDatabase("db2/"))
+                               .Build();
+  SchemaProto db3_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db3/email")
+                                            .SetDatabase("db3/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db3/message")
+                                            .SetDatabase("db3/"))
+                               .Build();
   // Set schema for db1
   SchemaStore::SetSchemaResult result;
   result.success = true;
-  result.schema_types_new_by_name.insert("db1_email");
-  result.schema_types_new_by_name.insert("db1_message");
+  result.schema_types_new_by_name.insert("db1/email");
+  result.schema_types_new_by_name.insert("db1/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db1_schema, /*database=*/"db1",
+                  db1_schema, /*database=*/"db1/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   // Set schema for db2
   result = SchemaStore::SetSchemaResult();
   result.success = true;
-  result.schema_types_new_by_name.insert("db2_email");
-  result.schema_types_new_by_name.insert("db2_message");
+  result.schema_types_new_by_name.insert("db2/email");
+  result.schema_types_new_by_name.insert("db2/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db2_schema, /*database=*/"db2",
+                  db2_schema, /*database=*/"db2/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   // Set schema for db3
   result = SchemaStore::SetSchemaResult();
   result.success = true;
-  result.schema_types_new_by_name.insert("db3_email");
-  result.schema_types_new_by_name.insert("db3_message");
+  result.schema_types_new_by_name.insert("db3/email");
+  result.schema_types_new_by_name.insert("db3/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db3_schema, /*database=*/"db3",
+                  db3_schema, /*database=*/"db3/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
-  // Set schema again for db2 and add a type. The added type should be appended
-  // to the end of the SchemaProto.
-  db2_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_recipient")
-                       .SetDatabase("db2"))
-          .Build();
-  result = SchemaStore::SetSchemaResult();
-  result.success = true;
-  result.schema_types_new_by_name.insert("db2_recipient");
-  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db2_schema, /*database=*/"db2",
-                  /*ignore_errors_and_delete_documents=*/false)),
-              IsOkAndHolds(EqualsSetSchemaResult(result)));
+
   SchemaProto expected_full_schema =
       SchemaBuilder()
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_email")  // SchemaTypeId 0
-                       .SetDatabase("db1"))
+                       .SetType("db1/email")  // SchemaTypeId 0
+                       .SetDatabase("db1/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")  // SchemaTypeId 1
-                       .SetDatabase("db1"))
+                       .SetType("db1/message")  // SchemaTypeId 1
+                       .SetDatabase("db1/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_email")  // SchemaTypeId 2
-                       .SetDatabase("db2"))
+                       .SetType("db2/email")  // SchemaTypeId 2
+                       .SetDatabase("db2/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")  // SchemaTypeId 3
-                       .SetDatabase("db2"))
+                       .SetType("db2/message")  // SchemaTypeId 3
+                       .SetDatabase("db2/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db3_email")  // SchemaTypeId 4
-                       .SetDatabase("db3"))
+                       .SetType("db3/email")  // SchemaTypeId 4
+                       .SetDatabase("db3/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db3_message")  // SchemaTypeId 5
-                       .SetDatabase("db3"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_recipient")  // SchemaTypeId 6
-                       .SetDatabase("db2"))
+                       .SetType("db3/message")  // SchemaTypeId 5
+                       .SetDatabase("db3/"))
           .Build();
   // Verify schema.
   ICING_ASSERT_OK_AND_ASSIGN(const SchemaProto* actual_full_schema,
                              schema_store->GetSchema());
   EXPECT_THAT(*actual_full_schema, EqualsProto(expected_full_schema));
 
-  // Reset db2 and delete some types. All types that were originally added after
-  // db2 should have their type ids changed.
+  // Reset db2 and delete some types. With the type id optimization, only the
+  // last few type ids should be changed.
   db2_schema = SchemaBuilder()
                    .AddType(SchemaTypeConfigBuilder()
-                                .SetType("db2_message")
-                                .SetDatabase("db2"))
+                                .SetType("db2/message")
+                                .SetDatabase("db2/"))
                    .Build();
-  expected_full_schema =
+  if (feature_flags_->enable_schema_type_id_optimization()) {
+    expected_full_schema =
+        SchemaBuilder()
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db1/email")  // SchemaTypeId 0
+                         .SetDatabase("db1/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db1/message")  // SchemaTypeId 1
+                         .SetDatabase("db1/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db3/message")  // SchemaTypeId 5 -> 2
+                         .SetDatabase("db3/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db2/message")  // SchemaTypeId 3 (unchanged)
+                         .SetDatabase("db2/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db3/email")  // SchemaTypeId 4 (unchanged)
+                         .SetDatabase("db3/"))
+            .Build();
+    result = SchemaStore::SetSchemaResult();
+    result.success = true;
+    result.schema_types_deleted_by_name.insert("db2/email");
+    result.schema_types_deleted_by_id.insert(2);   // db2_email
+    result.old_schema_type_ids_changed.insert(5);  // db3_message
+    EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                    db2_schema, /*database=*/"db2/",
+                    /*ignore_errors_and_delete_documents=*/true)),
+                IsOkAndHolds(EqualsSetSchemaResult(result)));
+    // Check the schema
+    EXPECT_THAT(schema_store->GetSchema(),
+                IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
+    EXPECT_THAT(schema_store->GetSchema("db1/"),
+                IsOkAndHolds(EqualsProto(db1_schema)));
+    EXPECT_THAT(schema_store->GetSchema("db2/"),
+                IsOkAndHolds(EqualsProto(db2_schema)));
+    EXPECT_THAT(
+        schema_store->GetSchema("db3/"),
+        IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                     .AddType(SchemaTypeConfigBuilder()
+                                                  .SetType("db3/message")
+                                                  .SetDatabase("db3/"))
+                                     .AddType(SchemaTypeConfigBuilder()
+                                                  .SetType("db3/email")
+                                                  .SetDatabase("db3/"))
+                                     .Build())));
+  } else {
+    expected_full_schema =
+        SchemaBuilder()
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db1/email")  // SchemaTypeId 0
+                         .SetDatabase("db1/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db1/message")  // SchemaTypeId 1
+                         .SetDatabase("db1/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db2/message")  // SchemaTypeId 3 -> 2
+                         .SetDatabase("db2/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db3/email")  // SchemaTypeId 4 -> 3
+                         .SetDatabase("db3/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db3/message")  // SchemaTypeId 5 -> 4
+                         .SetDatabase("db3/"))
+            .Build();
+    result = SchemaStore::SetSchemaResult();
+    result.success = true;
+    result.schema_types_deleted_by_name.insert("db2/email");
+    result.schema_types_deleted_by_id.insert(2);   // db2_email
+    result.old_schema_type_ids_changed.insert(3);  // db2_message
+    result.old_schema_type_ids_changed.insert(4);  // db3_email
+    result.old_schema_type_ids_changed.insert(5);  // db3_message
+    EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                    db2_schema, /*database=*/"db2/",
+                    /*ignore_errors_and_delete_documents=*/true)),
+                IsOkAndHolds(EqualsSetSchemaResult(result)));
+    // Check the schema
+    EXPECT_THAT(schema_store->GetSchema(),
+                IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
+    EXPECT_THAT(schema_store->GetSchema("db1/"),
+                IsOkAndHolds(EqualsProto(db1_schema)));
+    EXPECT_THAT(schema_store->GetSchema("db2/"),
+                IsOkAndHolds(EqualsProto(db2_schema)));
+    EXPECT_THAT(schema_store->GetSchema("db3/"),
+                IsOkAndHolds(EqualsProto(db3_schema)));
+  }
+}
+
+TEST_F(SchemaStoreTest, SetDatabaseDeletedTypesWithLastTypeRemovedOk) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*initialize_stats=*/nullptr));
+
+  // Set schema for the first time
+  SchemaProto full_schema =
       SchemaBuilder()
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_email")  // SchemaTypeId 0
-                       .SetDatabase("db1"))
+                       .SetType("db1/email")  // SchemaTypeId 0
+                       .SetDatabase("db1/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")  // SchemaTypeId 1
-                       .SetDatabase("db1"))
+                       .SetType("db1/message")  // SchemaTypeId 1
+                       .SetDatabase("db1/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")  // SchemaTypeId 2
-                       .SetDatabase("db2"))
+                       .SetType("db2/email")  // SchemaTypeId 2
+                       .SetDatabase("db2/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db3_email")  // SchemaTypeId 3
-                       .SetDatabase("db3"))
+                       .SetType("db2/message")  // SchemaTypeId 3
+                       .SetDatabase("db2/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db3_message")  // SchemaTypeId 4
-                       .SetDatabase("db3"))
+                       .SetType("db3/email")  // SchemaTypeId 4
+                       .SetDatabase("db3/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db3/message")  // SchemaTypeId 5
+                       .SetDatabase("db3/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2/recipient")  // SchemaTypeId 6
+                       .SetDatabase("db2/"))
           .Build();
+
+  // Set full schema using the empty db
+  EXPECT_TRUE(schema_store
+                  ->SetSchema(CreateSetSchemaRequestProto(
+                      full_schema, /*database=*/"",
+                      /*ignore_errors_and_delete_documents=*/false))
+                  .ok());
+  // Verify schema.
+  ICING_ASSERT_OK_AND_ASSIGN(const SchemaProto* actual_full_schema,
+                             schema_store->GetSchema());
+  EXPECT_THAT(*actual_full_schema, EqualsProto(full_schema));
+
+  // Reset db2 and delete some types. With the type id optimization, only the
+  // last few type ids should be changed.
+  SchemaProto db2_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/message")
+                                            .SetDatabase("db2/"))
+                               .Build();
+  if (feature_flags_->enable_schema_type_id_optimization()) {
+    SchemaProto expected_full_schema =
+        SchemaBuilder()
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db1/email")  // SchemaTypeId 0
+                         .SetDatabase("db1/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db1/message")  // SchemaTypeId 1
+                         .SetDatabase("db1/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db3/message")  // SchemaTypeId 5 -> 2
+                         .SetDatabase("db3/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db2/message")  // SchemaTypeId 3
+                         .SetDatabase("db2/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db3/email")  // SchemaTypeId 4
+                         .SetDatabase("db3/"))
+            .Build();
+    SchemaStore::SetSchemaResult result = SchemaStore::SetSchemaResult();
+    result.success = true;
+    result.schema_types_deleted_by_name.insert("db2/email");
+    result.schema_types_deleted_by_name.insert("db2/recipient");
+    result.schema_types_deleted_by_id.insert(
+        schema_store->GetSchemaTypeId("db2/email").ValueOrDie());  // 2
+    result.schema_types_deleted_by_id.insert(
+        schema_store->GetSchemaTypeId("db2/recipient").ValueOrDie());  // 6
+    // Only 1 type id is changed.
+    result.old_schema_type_ids_changed.insert(
+        schema_store->GetSchemaTypeId("db3/message").ValueOrDie());  // 5
+    EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                    db2_schema, /*database=*/"db2/",
+                    /*ignore_errors_and_delete_documents=*/true)),
+                IsOkAndHolds(EqualsSetSchemaResult(result)));
+
+    // Check the schema
+    EXPECT_THAT(schema_store->GetSchema(),
+                IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
+    EXPECT_THAT(
+        schema_store->GetSchema("db1/"),
+        IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                     .AddType(SchemaTypeConfigBuilder()
+                                                  .SetType("db1/email")
+                                                  .SetDatabase("db1/"))
+                                     .AddType(SchemaTypeConfigBuilder()
+                                                  .SetType("db1/message")
+                                                  .SetDatabase("db1/"))
+                                     .Build())));
+    EXPECT_THAT(
+        schema_store->GetSchema("db2/"),
+        IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                     .AddType(SchemaTypeConfigBuilder()
+                                                  .SetType("db2/message")
+                                                  .SetDatabase("db2/"))
+                                     .Build())));
+    EXPECT_THAT(
+        schema_store->GetSchema("db3/"),
+        IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                     .AddType(SchemaTypeConfigBuilder()
+                                                  .SetType("db3/message")
+                                                  .SetDatabase("db3/"))
+                                     .AddType(SchemaTypeConfigBuilder()
+                                                  .SetType("db3/email")
+                                                  .SetDatabase("db3/"))
+                                     .Build())));
+  } else {
+    SchemaProto expected_full_schema =
+        SchemaBuilder()
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db1/email")  // SchemaTypeId 0
+                         .SetDatabase("db1/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db1/message")  // SchemaTypeId 1
+                         .SetDatabase("db1/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db2/message")  // SchemaTypeId 3 -> 2
+                         .SetDatabase("db2/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db3/email")  // SchemaTypeId 4 -> 3
+                         .SetDatabase("db3/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db3/message")  // SchemaTypeId 5 -> 4
+                         .SetDatabase("db3/"))
+            .Build();
+    SchemaStore::SetSchemaResult result = SchemaStore::SetSchemaResult();
+    result.success = true;
+    result.success = true;
+    result.schema_types_deleted_by_name.insert("db2/email");
+    result.schema_types_deleted_by_name.insert("db2/recipient");
+    result.schema_types_deleted_by_id.insert(
+        schema_store->GetSchemaTypeId("db2/email").ValueOrDie());  // 2
+    result.schema_types_deleted_by_id.insert(
+        schema_store->GetSchemaTypeId("db2/recipient").ValueOrDie());  // 6
+    // 3 type ids are changed
+    result.old_schema_type_ids_changed.insert(
+        schema_store->GetSchemaTypeId("db2/message")
+            .ValueOrDie());  // db2_message
+    result.old_schema_type_ids_changed.insert(
+        schema_store->GetSchemaTypeId("db3/email").ValueOrDie());  // db3_email
+    result.old_schema_type_ids_changed.insert(
+        schema_store->GetSchemaTypeId("db3/message")
+            .ValueOrDie());  // db3_message
+
+    EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                    db2_schema, /*database=*/"db2/",
+                    /*ignore_errors_and_delete_documents=*/true)),
+                IsOkAndHolds(EqualsSetSchemaResult(result)));
+
+    // Check the schema
+    EXPECT_THAT(schema_store->GetSchema(),
+                IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
+    EXPECT_THAT(
+        schema_store->GetSchema("db1/"),
+        IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                     .AddType(SchemaTypeConfigBuilder()
+                                                  .SetType("db1/email")
+                                                  .SetDatabase("db1/"))
+                                     .AddType(SchemaTypeConfigBuilder()
+                                                  .SetType("db1/message")
+                                                  .SetDatabase("db1/"))
+                                     .Build())));
+    EXPECT_THAT(
+        schema_store->GetSchema("db2/"),
+        IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                     .AddType(SchemaTypeConfigBuilder()
+                                                  .SetType("db2/message")
+                                                  .SetDatabase("db2/"))
+                                     .Build())));
+    EXPECT_THAT(
+        schema_store->GetSchema("db3/"),
+        IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                     .AddType(SchemaTypeConfigBuilder()
+                                                  .SetType("db3/email")
+                                                  .SetDatabase("db3/"))
+                                     .AddType(SchemaTypeConfigBuilder()
+                                                  .SetType("db3/message")
+                                                  .SetDatabase("db3/"))
+                                     .Build())));
+  }
+}
+
+TEST_F(SchemaStoreTest, SetDatabaseDeletedTypesWithGapsOk) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*initialize_stats=*/nullptr));
+
+  // Set schema for the first time
+  SchemaProto expected_full_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db1/type1")  // SchemaTypeId 0
+                       .SetDatabase("db1/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db1/type2")  // SchemaTypeId 1
+                       .SetDatabase("db1/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2/type1")  // SchemaTypeId 2
+                       .SetDatabase("db2/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2/type2")  // SchemaTypeId 3
+                       .SetDatabase("db2/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2/type3")  // SchemaTypeId 4
+                       .SetDatabase("db2/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db3/type1")  // SchemaTypeId 5
+                       .SetDatabase("db3/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2/type4")  // SchemaTypeId 6
+                       .SetDatabase("db2/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2/type5")  // SchemaTypeId 7
+                       .SetDatabase("db2/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db3/type2")  // SchemaTypeId 8
+                       .SetDatabase("db3/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db3/type3")  // SchemaTypeId 9
+                       .SetDatabase("db3/"))
+          .Build();
+
+  // Set full schema using the empty db
+  EXPECT_TRUE(schema_store
+                  ->SetSchema(CreateSetSchemaRequestProto(
+                      expected_full_schema, /*database=*/"",
+                      /*ignore_errors_and_delete_documents=*/false))
+                  .ok());
+  // Verify schema.
+  ICING_ASSERT_OK_AND_ASSIGN(const SchemaProto* actual_full_schema,
+                             schema_store->GetSchema());
+  EXPECT_THAT(*actual_full_schema, EqualsProto(expected_full_schema));
+
+  // Set schema again for db2 and delete some types.
+  SchemaProto db2_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/type2")
+                                            .SetDatabase("db2/"))
+                               .Build();
+
+  if (feature_flags_->enable_schema_type_id_optimization()) {
+    expected_full_schema =
+        SchemaBuilder()
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db1/type1")  // SchemaTypeId 0
+                         .SetDatabase("db1/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db1/type2")  // SchemaTypeId 1
+                         .SetDatabase("db1/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db3/type2")  // SchemaTypeId 8 -> 2
+                         .SetDatabase("db3/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db2/type2")  // SchemaTypeId 3
+                         .SetDatabase("db2/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db3/type3")  // SchemaTypeId 9 -> 4
+                         .SetDatabase("db3/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db3/type1")  // SchemaTypeId 5
+                         .SetDatabase("db3/"))
+            .Build();
+
+    SchemaStore::SetSchemaResult result = SchemaStore::SetSchemaResult();
+    result.success = true;
+    result.schema_types_deleted_by_name.insert("db2/type1");
+    result.schema_types_deleted_by_name.insert("db2/type3");
+    result.schema_types_deleted_by_name.insert("db2/type4");
+    result.schema_types_deleted_by_name.insert("db2/type5");
+    result.schema_types_deleted_by_id.insert(
+        schema_store->GetSchemaTypeId("db2/type1").ValueOrDie());  // 2
+    result.schema_types_deleted_by_id.insert(
+        schema_store->GetSchemaTypeId("db2/type3").ValueOrDie());  // 4
+    result.schema_types_deleted_by_id.insert(
+        schema_store->GetSchemaTypeId("db2/type4").ValueOrDie());  // 6
+    result.schema_types_deleted_by_id.insert(
+        schema_store->GetSchemaTypeId("db2/type5").ValueOrDie());  // 7
+
+    // Only 2 schema type ids are changed since we move the fast few types at
+    // the end of the original schema to fill the gaps of the deleted types.
+    result.old_schema_type_ids_changed.insert(
+        schema_store->GetSchemaTypeId("db3/type2").ValueOrDie());  // 8
+    result.old_schema_type_ids_changed.insert(
+        schema_store->GetSchemaTypeId("db3/type3").ValueOrDie());  // 9
+
+    EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                    db2_schema, /*database=*/"db2/",
+                    /*ignore_errors_and_delete_documents=*/true)),
+                IsOkAndHolds(EqualsSetSchemaResult(result)));
+
+    // Check the schema
+    EXPECT_THAT(schema_store->GetSchema(),
+                IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
+
+    EXPECT_THAT(schema_store->GetSchema("db1/"),
+                IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                             .AddType(SchemaTypeConfigBuilder()
+                                                          .SetType("db1/type1")
+                                                          .SetDatabase("db1/"))
+                                             .AddType(SchemaTypeConfigBuilder()
+                                                          .SetType("db1/type2")
+                                                          .SetDatabase("db1/"))
+                                             .Build())));
+    EXPECT_THAT(schema_store->GetSchema("db2/"),
+                IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                             .AddType(SchemaTypeConfigBuilder()
+                                                          .SetType("db2/type2")
+                                                          .SetDatabase("db2/"))
+                                             .Build())));
+    EXPECT_THAT(schema_store->GetSchema("db3/"),
+                IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                             .AddType(SchemaTypeConfigBuilder()
+                                                          .SetType("db3/type2")
+                                                          .SetDatabase("db3/"))
+                                             .AddType(SchemaTypeConfigBuilder()
+                                                          .SetType("db3/type3")
+                                                          .SetDatabase("db3/"))
+                                             .AddType(SchemaTypeConfigBuilder()
+                                                          .SetType("db3/type1")
+                                                          .SetDatabase("db3/"))
+                                             .Build())));
+  } else {
+    expected_full_schema =
+        SchemaBuilder()
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db1/type1")  // SchemaTypeId 0
+                         .SetDatabase("db1/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db1/type2")  // SchemaTypeId 1
+                         .SetDatabase("db1/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db2/type2")  // SchemaTypeId 3 -> 2
+                         .SetDatabase("db2/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db3/type1")  // SchemaTypeId 5 -> 3
+                         .SetDatabase("db3/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db3/type2")  // SchemaTypeId 8 -> 4
+                         .SetDatabase("db3/"))
+            .AddType(SchemaTypeConfigBuilder()
+                         .SetType("db3/type3")  // SchemaTypeId 9 -> 5
+                         .SetDatabase("db3/"))
+            .Build();
+    SchemaStore::SetSchemaResult result = SchemaStore::SetSchemaResult();
+    result.success = true;
+    result.schema_types_deleted_by_name.insert("db2/type1");
+    result.schema_types_deleted_by_name.insert("db2/type3");
+    result.schema_types_deleted_by_name.insert("db2/type4");
+    result.schema_types_deleted_by_name.insert("db2/type5");
+    result.schema_types_deleted_by_id.insert(
+        schema_store->GetSchemaTypeId("db2/type1").ValueOrDie());  // 2
+    result.schema_types_deleted_by_id.insert(
+        schema_store->GetSchemaTypeId("db2/type3").ValueOrDie());  // 4
+    result.schema_types_deleted_by_id.insert(
+        schema_store->GetSchemaTypeId("db2/type4").ValueOrDie());  // 6
+    result.schema_types_deleted_by_id.insert(
+        schema_store->GetSchemaTypeId("db2/type5").ValueOrDie());  // 7
+
+    // 4 type ids are changed since the type ids are assigned based on the
+    // order of the types in the existing schema.
+    result.old_schema_type_ids_changed.insert(
+        schema_store->GetSchemaTypeId("db2/type2").ValueOrDie());  // 3
+    result.old_schema_type_ids_changed.insert(
+        schema_store->GetSchemaTypeId("db3/type1").ValueOrDie());  // 5
+    result.old_schema_type_ids_changed.insert(
+        schema_store->GetSchemaTypeId("db3/type2").ValueOrDie());  // 8
+    result.old_schema_type_ids_changed.insert(
+        schema_store->GetSchemaTypeId("db3/type3").ValueOrDie());  // 9
+
+    EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                    db2_schema, /*database=*/"db2/",
+                    /*ignore_errors_and_delete_documents=*/true)),
+                IsOkAndHolds(EqualsSetSchemaResult(result)));
+
+    // Check the schema
+    EXPECT_THAT(schema_store->GetSchema(),
+                IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
+
+    EXPECT_THAT(schema_store->GetSchema("db1/"),
+                IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                             .AddType(SchemaTypeConfigBuilder()
+                                                          .SetType("db1/type1")
+                                                          .SetDatabase("db1/"))
+                                             .AddType(SchemaTypeConfigBuilder()
+                                                          .SetType("db1/type2")
+                                                          .SetDatabase("db1/"))
+                                             .Build())));
+    EXPECT_THAT(schema_store->GetSchema("db2/"),
+                IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                             .AddType(SchemaTypeConfigBuilder()
+                                                          .SetType("db2/type2")
+                                                          .SetDatabase("db2/"))
+                                             .Build())));
+    EXPECT_THAT(schema_store->GetSchema("db3/"),
+                IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                             .AddType(SchemaTypeConfigBuilder()
+                                                          .SetType("db3/type1")
+                                                          .SetDatabase("db3/"))
+                                             .AddType(SchemaTypeConfigBuilder()
+                                                          .SetType("db3/type2")
+                                                          .SetDatabase("db3/"))
+                                             .AddType(SchemaTypeConfigBuilder()
+                                                          .SetType("db3/type3")
+                                                          .SetDatabase("db3/"))
+                                             .Build())));
+  }
+}
+
+TEST_F(SchemaStoreTest, SetDatabaseRenamedTypesPreserveTypeIds) {
+  if (!feature_flags_->enable_schema_type_id_optimization()) {
+    GTEST_SKIP() << "Test for schema type id optimization.";
+  }
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*initialize_stats=*/nullptr));
+
+  // Set schema for the first time
+  SchemaProto db1_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/email")
+                                            .SetDatabase("db1/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/message")
+                                            .SetDatabase("db1/"))
+                               .Build();
+  SchemaProto db2_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/email")
+                                            .SetDatabase("db2/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/message")
+                                            .SetDatabase("db2/"))
+                               .Build();
+  SchemaProto db3_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db3/email")
+                                            .SetDatabase("db3/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db3/message")
+                                            .SetDatabase("db3/"))
+                               .Build();
+
+  // Set schema for db1
+  SchemaStore::SetSchemaResult result;
+  result.success = true;
+  result.schema_types_new_by_name.insert("db1/email");
+  result.schema_types_new_by_name.insert("db1/message");
+  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                  db1_schema, /*database=*/"db1/",
+                  /*ignore_errors_and_delete_documents=*/false)),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+  // Set schema for db2
   result = SchemaStore::SetSchemaResult();
   result.success = true;
-  result.schema_types_deleted_by_name.insert("db2_email");
-  result.schema_types_deleted_by_name.insert("db2_recipient");
-  result.schema_types_deleted_by_id.insert(2);   // db2_email
-  result.schema_types_deleted_by_id.insert(6);   // db2_recipient
-  result.old_schema_type_ids_changed.insert(3);  // db2_message
-  result.old_schema_type_ids_changed.insert(4);  // db3_email
-  result.old_schema_type_ids_changed.insert(5);  // db3_message
+  result.schema_types_new_by_name.insert("db2/email");
+  result.schema_types_new_by_name.insert("db2/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db2_schema, /*database=*/"db2",
+                  db2_schema, /*database=*/"db2/",
+                  /*ignore_errors_and_delete_documents=*/false)),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+  // Set schema for db3
+  result = SchemaStore::SetSchemaResult();
+  result.success = true;
+  result.schema_types_new_by_name.insert("db3/email");
+  result.schema_types_new_by_name.insert("db3/message");
+  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                  db3_schema, /*database=*/"db3/",
+                  /*ignore_errors_and_delete_documents=*/false)),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+
+  // Set schema again for db2 and rename email -> email2. The type ids of other
+  // existing types should not change.
+  db2_schema = SchemaBuilder()
+                   .AddType(SchemaTypeConfigBuilder()
+                                .SetType("db2/message")
+                                .SetDatabase("db2/"))
+                   .AddType(SchemaTypeConfigBuilder()
+                                .SetType("db2/email2")
+                                .SetDatabase("db2/"))
+                   .Build();
+  result = SchemaStore::SetSchemaResult();
+  result.success = true;
+  result.schema_types_deleted_by_name.insert("db2/email");
+  result.schema_types_deleted_by_id.insert(2);  // db2_email
+  result.schema_types_new_by_name.insert("db2/email2");
+  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                  db2_schema, /*database=*/"db2/",
                   /*ignore_errors_and_delete_documents=*/true)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
 
-  // Check the schema
+  SchemaProto expected_full_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db1/email")  // SchemaTypeId 0
+                       .SetDatabase("db1/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db1/message")  // SchemaTypeId 1
+                       .SetDatabase("db1/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2/email2")  // SchemaTypeId 2
+                       .SetDatabase("db2/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2/message")  // SchemaTypeId 3
+                       .SetDatabase("db2/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db3/email")  // SchemaTypeId 4
+                       .SetDatabase("db3/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db3/message")  // SchemaTypeId 5
+                       .SetDatabase("db3/"))
+          .Build();
+
+  // Verify schema.
+  ICING_ASSERT_OK_AND_ASSIGN(const SchemaProto* actual_full_schema,
+                             schema_store->GetSchema());
+  EXPECT_THAT(*actual_full_schema, EqualsProto(expected_full_schema));
+
+  // Check the schema for each database.
   EXPECT_THAT(schema_store->GetSchema(),
               IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
-  EXPECT_THAT(schema_store->GetSchema("db1"),
+  EXPECT_THAT(schema_store->GetSchema("db1/"),
               IsOkAndHolds(EqualsProto(db1_schema)));
-  EXPECT_THAT(schema_store->GetSchema("db2"),
-              IsOkAndHolds(EqualsProto(db2_schema)));
-  EXPECT_THAT(schema_store->GetSchema("db3"),
+  EXPECT_THAT(schema_store->GetSchema("db2/"),
+              IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                           .AddType(SchemaTypeConfigBuilder()
+                                                        .SetType("db2/email2")
+                                                        .SetDatabase("db2/"))
+                                           .AddType(SchemaTypeConfigBuilder()
+                                                        .SetType("db2/message")
+                                                        .SetDatabase("db2/"))
+                                           .Build())));
+  EXPECT_THAT(schema_store->GetSchema("db3/"),
               IsOkAndHolds(EqualsProto(db3_schema)));
 }
 
+TEST_F(SchemaStoreTest, SetDatabaseAddMoreTypesThanDeletedPreserveTypeIds) {
+  if (!feature_flags_->enable_schema_type_id_optimization()) {
+    GTEST_SKIP() << "Test for schema type id optimization.";
+  }
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*initialize_stats=*/nullptr));
+
+  // Set schema for the first time
+  SchemaProto db1_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/email")
+                                            .SetDatabase("db1/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/message")
+                                            .SetDatabase("db1/"))
+                               .Build();
+  SchemaProto db2_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/email")
+                                            .SetDatabase("db2/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/message")
+                                            .SetDatabase("db2/"))
+                               .Build();
+  SchemaProto db3_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db3/email")
+                                            .SetDatabase("db3/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db3/message")
+                                            .SetDatabase("db3/"))
+                               .Build();
+
+  // Set schema for db1
+  SchemaStore::SetSchemaResult result;
+  result.success = true;
+  result.schema_types_new_by_name.insert("db1/email");
+  result.schema_types_new_by_name.insert("db1/message");
+  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                  db1_schema, /*database=*/"db1/",
+                  /*ignore_errors_and_delete_documents=*/false)),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+  // Set schema for db2
+  result = SchemaStore::SetSchemaResult();
+  result.success = true;
+  result.schema_types_new_by_name.insert("db2/email");
+  result.schema_types_new_by_name.insert("db2/message");
+  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                  db2_schema, /*database=*/"db2/",
+                  /*ignore_errors_and_delete_documents=*/false)),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+  // Set schema for db3
+  result = SchemaStore::SetSchemaResult();
+  result.success = true;
+  result.schema_types_new_by_name.insert("db3/email");
+  result.schema_types_new_by_name.insert("db3/message");
+  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                  db3_schema, /*database=*/"db3/",
+                  /*ignore_errors_and_delete_documents=*/false)),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+
+  // Set schema again for db2. Delete the email type, and add 2 more types.
+  // The type id of existing types should not change.
+  db2_schema = SchemaBuilder()
+                   .AddType(SchemaTypeConfigBuilder()
+                                .SetType("db2/message")
+                                .SetDatabase("db2/"))
+                   .AddType(SchemaTypeConfigBuilder()
+                                .SetType("db2/new_type1")
+                                .SetDatabase("db2/"))
+                   .AddType(SchemaTypeConfigBuilder()
+                                .SetType("db2/new_type2")
+                                .SetDatabase("db2/"))
+                   .Build();
+  result = SchemaStore::SetSchemaResult();
+  result.success = true;
+  result.schema_types_deleted_by_name.insert("db2/email");
+  result.schema_types_deleted_by_id.insert(2);  // db2_email
+  result.schema_types_new_by_name.insert("db2/new_type1");
+  result.schema_types_new_by_name.insert("db2/new_type2");
+  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                  db2_schema, /*database=*/"db2/",
+                  /*ignore_errors_and_delete_documents=*/true)),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+
+  SchemaProto expected_full_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db1/email")  // SchemaTypeId 0
+                       .SetDatabase("db1/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db1/message")  // SchemaTypeId 1
+                       .SetDatabase("db1/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2/new_type1")  // SchemaTypeId 2
+                       .SetDatabase("db2/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2/message")  // SchemaTypeId 3
+                       .SetDatabase("db2/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db3/email")  // SchemaTypeId 4
+                       .SetDatabase("db3/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db3/message")  // SchemaTypeId 5
+                       .SetDatabase("db3/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2/new_type2")  // SchemaTypeId 6
+                       .SetDatabase("db2/"))
+          .Build();
+
+  // Verify schema.
+  ICING_ASSERT_OK_AND_ASSIGN(const SchemaProto* actual_full_schema,
+                             schema_store->GetSchema());
+  EXPECT_THAT(*actual_full_schema, EqualsProto(expected_full_schema));
+
+  // Check the schema for each database.
+  EXPECT_THAT(schema_store->GetSchema(),
+              IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
+  EXPECT_THAT(schema_store->GetSchema("db1/"),
+              IsOkAndHolds(EqualsProto(db1_schema)));
+  EXPECT_THAT(
+      schema_store->GetSchema("db2/"),
+      IsOkAndHolds(EqualsProto(SchemaBuilder()
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db2/new_type1")
+                                                .SetDatabase("db2/"))
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db2/message")
+                                                .SetDatabase("db2/"))
+                                   .AddType(SchemaTypeConfigBuilder()
+                                                .SetType("db2/new_type2")
+                                                .SetDatabase("db2/"))
+                                   .Build())));
+  EXPECT_THAT(schema_store->GetSchema("db3/"),
+              IsOkAndHolds(EqualsProto(db3_schema)));
+}
+
+TEST_F(SchemaStoreTest, SetEmptySchemaOk) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*initialize_stats=*/nullptr));
+
+  // Set schema for the first time
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder().SetType("db/email").SetDatabase("db/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db/message")
+                       .SetDatabase("db/"))
+          .Build();
+  SchemaStore::SetSchemaResult result;
+  result.success = true;
+  result.schema_types_new_by_name.insert("db/email");
+  result.schema_types_new_by_name.insert("db/message");
+  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                  schema, /*database=*/"db/",
+                  /*ignore_errors_and_delete_documents=*/false)),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+
+  // Verify schema.
+  EXPECT_THAT(schema_store->GetSchema(),
+              IsOkAndHolds(Pointee(EqualsProto(schema))));
+
+  // Reset to an empty schema.
+  result = SchemaStore::SetSchemaResult();
+  result.success = true;
+  result.schema_types_deleted_by_name.insert("db/email");
+  result.schema_types_deleted_by_name.insert("db/message");
+  result.schema_types_deleted_by_id.insert(0);  // email
+  result.schema_types_deleted_by_id.insert(1);  // message
+  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
+                  SchemaProto(), /*database=*/"db/",
+                  /*ignore_errors_and_delete_documents=*/true)),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+
+  // Check the schema. It should be empty.
+  EXPECT_THAT(schema_store->GetSchema(),
+              IsOkAndHolds(Pointee(EqualsProto(SchemaProto()))));
+
+  EXPECT_THAT(schema_store->PersistToDisk(), IsOk());
+}
+
 TEST_F(SchemaStoreTest, SetEmptySchemaClearsDatabase) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
@@ -1264,79 +2404,79 @@
                           /*initialize_stats=*/nullptr));
 
   // Set schema for the first time
-  SchemaProto db1_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .Build();
-  SchemaProto db2_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .Build();
-  SchemaProto db3_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db3_email").SetDatabase("db3"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db3_message")
-                       .SetDatabase("db3"))
-          .Build();
+  SchemaProto db1_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/email")
+                                            .SetDatabase("db1/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/message")
+                                            .SetDatabase("db1/"))
+                               .Build();
+  SchemaProto db2_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/email")
+                                            .SetDatabase("db2/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/message")
+                                            .SetDatabase("db2/"))
+                               .Build();
+  SchemaProto db3_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db3/email")
+                                            .SetDatabase("db3/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db3/message")
+                                            .SetDatabase("db3/"))
+                               .Build();
 
   // Set schema for db1
   SchemaStore::SetSchemaResult result;
   result.success = true;
-  result.schema_types_new_by_name.insert("db1_email");
-  result.schema_types_new_by_name.insert("db1_message");
+  result.schema_types_new_by_name.insert("db1/email");
+  result.schema_types_new_by_name.insert("db1/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db1_schema, /*database=*/"db1",
+                  db1_schema, /*database=*/"db1/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   // Set schema for db2
   result = SchemaStore::SetSchemaResult();
   result.success = true;
-  result.schema_types_new_by_name.insert("db2_email");
-  result.schema_types_new_by_name.insert("db2_message");
+  result.schema_types_new_by_name.insert("db2/email");
+  result.schema_types_new_by_name.insert("db2/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db2_schema, /*database=*/"db2",
+                  db2_schema, /*database=*/"db2/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   // Set schema for db3
   result = SchemaStore::SetSchemaResult();
   result.success = true;
-  result.schema_types_new_by_name.insert("db3_email");
-  result.schema_types_new_by_name.insert("db3_message");
+  result.schema_types_new_by_name.insert("db3/email");
+  result.schema_types_new_by_name.insert("db3/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db3_schema, /*database=*/"db3",
+                  db3_schema, /*database=*/"db3/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   // Verify schema.
   SchemaProto expected_full_schema =
       SchemaBuilder()
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_email")  // SchemaTypeId 0
-                       .SetDatabase("db1"))
+                       .SetType("db1/email")  // SchemaTypeId 0
+                       .SetDatabase("db1/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")  // SchemaTypeId 1
-                       .SetDatabase("db1"))
+                       .SetType("db1/message")  // SchemaTypeId 1
+                       .SetDatabase("db1/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_email")  // SchemaTypeId 2
-                       .SetDatabase("db2"))
+                       .SetType("db2/email")  // SchemaTypeId 2
+                       .SetDatabase("db2/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")  // SchemaTypeId 3
-                       .SetDatabase("db2"))
+                       .SetType("db2/message")  // SchemaTypeId 3
+                       .SetDatabase("db2/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db3_email")  // SchemaTypeId 4
-                       .SetDatabase("db3"))
+                       .SetType("db3/email")  // SchemaTypeId 4
+                       .SetDatabase("db3/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db3_message")  // SchemaTypeId 5
-                       .SetDatabase("db3"))
+                       .SetType("db3/message")  // SchemaTypeId 5
+                       .SetDatabase("db3/"))
           .Build();
   EXPECT_THAT(schema_store->GetSchema(),
               IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
@@ -1347,40 +2487,70 @@
   db2_schema = SchemaProto();
   result = SchemaStore::SetSchemaResult();
   result.success = true;
-  result.schema_types_deleted_by_name.insert("db2_email");
-  result.schema_types_deleted_by_name.insert("db2_message");
+  result.schema_types_deleted_by_name.insert("db2/email");
+  result.schema_types_deleted_by_name.insert("db2/message");
   result.schema_types_deleted_by_id.insert(2);   // db2_email
   result.schema_types_deleted_by_id.insert(3);   // db2_message
   result.old_schema_type_ids_changed.insert(4);  // db3_email
   result.old_schema_type_ids_changed.insert(5);  // db3_message
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db2_schema, /*database=*/"db2",
+                  db2_schema, /*database=*/"db2/",
                   /*ignore_errors_and_delete_documents=*/true)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
 
-  // Check the schema. Schemas for db1 and db3 should be unchanged.
-  EXPECT_THAT(schema_store->GetSchema("db1"),
+  // Check the schema. Schema types for db1 and db3 should be unchanged, but if
+  // schema_type_id_optimization is enabled, then the schema type order for db3
+  // will change since the position of the deleted types from db2 will be
+  // backfilled from the last type in the original schema.
+  EXPECT_THAT(schema_store->GetSchema("db1/"),
               IsOkAndHolds(EqualsProto(db1_schema)));
-  EXPECT_THAT(schema_store->GetSchema("db3"),
+  if (feature_flags_->enable_schema_type_id_optimization()) {
+    db3_schema = SchemaBuilder()
+                     .AddType(SchemaTypeConfigBuilder()
+                                  .SetType("db3/email")
+                                  .SetDatabase("db3/"))
+                     .AddType(SchemaTypeConfigBuilder()
+                                  .SetType("db3/message")
+                                  .SetDatabase("db3/"))
+                     .Build();
+    expected_full_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/email")
+                                            .SetDatabase("db1/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/message")
+                                            .SetDatabase("db1/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db3/email")
+                                            .SetDatabase("db3/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db3/message")
+                                            .SetDatabase("db3/"))
+                               .Build();
+  } else {
+    expected_full_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/email")
+                                            .SetDatabase("db1/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/message")
+                                            .SetDatabase("db1/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db3/email")
+                                            .SetDatabase("db3/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db3/message")
+                                            .SetDatabase("db3/"))
+                               .Build();
+  }
+  EXPECT_THAT(schema_store->GetSchema("db3/"),
               IsOkAndHolds(EqualsProto(db3_schema)));
 
   // GetSchema for db2 should return NotFoundError
-  EXPECT_THAT(schema_store->GetSchema("db2"),
+  EXPECT_THAT(schema_store->GetSchema("db2/"),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
-  expected_full_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db3_email").SetDatabase("db3"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db3_message")
-                       .SetDatabase("db3"))
-          .Build();
+  // Get full schema.
   EXPECT_THAT(schema_store->GetSchema(),
               IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
 }
@@ -1423,49 +2593,50 @@
                           /*initialize_stats=*/nullptr));
 
   // Set schema for the first time
-  SchemaProto db1_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .Build();
-  SchemaProto db2_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .Build();
-  SchemaProto expected_full_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .Build();
+  SchemaProto db1_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/email")
+                                            .SetDatabase("db1/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/message")
+                                            .SetDatabase("db1/"))
+                               .Build();
+  SchemaProto db2_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/email")
+                                            .SetDatabase("db2/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/message")
+                                            .SetDatabase("db2/"))
+                               .Build();
+  SchemaProto expected_full_schema = SchemaBuilder()
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db1/email")
+                                                      .SetDatabase("db1/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db1/message")
+                                                      .SetDatabase("db1/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/email")
+                                                      .SetDatabase("db2/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/message")
+                                                      .SetDatabase("db2/"))
+                                         .Build();
   SchemaStore::SetSchemaResult result;
   result.success = true;
-  result.schema_types_new_by_name.insert("db1_email");
-  result.schema_types_new_by_name.insert("db1_message");
+  result.schema_types_new_by_name.insert("db1/email");
+  result.schema_types_new_by_name.insert("db1/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db1_schema, /*database=*/"db1",
+                  db1_schema, /*database=*/"db1/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   result = SchemaStore::SetSchemaResult();
   result.success = true;
-  result.schema_types_new_by_name.insert("db2_email");
-  result.schema_types_new_by_name.insert("db2_message");
+  result.schema_types_new_by_name.insert("db2/email");
+  result.schema_types_new_by_name.insert("db2/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db2_schema, /*database=*/"db2",
+                  db2_schema, /*database=*/"db2/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   ICING_ASSERT_OK_AND_ASSIGN(const SchemaProto* actual_full_schema,
@@ -1475,28 +2646,29 @@
   // Make db2 incompatible by changing a type name
   SchemaProto db2_schema_incompatible =
       SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_recipient")
-                       .SetDatabase("db2"))
+                       .SetType("db2/email")
+                       .SetDatabase("db2/"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2/recipient")
+                       .SetDatabase("db2/"))
           .Build();
   result = SchemaStore::SetSchemaResult();
   result.success = false;
-  result.schema_types_deleted_by_name.insert("db2_message");
-  result.schema_types_new_by_name.insert("db2_recipient");
+  result.schema_types_deleted_by_name.insert("db2/message");
+  result.schema_types_new_by_name.insert("db2/recipient");
   result.schema_types_deleted_by_id.insert(3);  // db2_message
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db2_schema_incompatible, /*database=*/"db2",
+                  db2_schema_incompatible, /*database=*/"db2/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
 
   // Check the schema, this should not have changed
   EXPECT_THAT(schema_store->GetSchema(),
               IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
-  EXPECT_THAT(schema_store->GetSchema("db1"),
+  EXPECT_THAT(schema_store->GetSchema("db1/"),
               IsOkAndHolds(EqualsProto(db1_schema)));
-  EXPECT_THAT(schema_store->GetSchema("db2"),
+  EXPECT_THAT(schema_store->GetSchema("db2/"),
               IsOkAndHolds(EqualsProto(db2_schema)));
 }
 
@@ -1508,49 +2680,50 @@
                           /*initialize_stats=*/nullptr));
 
   // Set schema for the first time
-  SchemaProto db1_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .Build();
-  SchemaProto db2_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .Build();
-  SchemaProto expected_full_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .Build();
+  SchemaProto db1_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/email")
+                                            .SetDatabase("db1/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db1/message")
+                                            .SetDatabase("db1/"))
+                               .Build();
+  SchemaProto db2_schema = SchemaBuilder()
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/email")
+                                            .SetDatabase("db2/"))
+                               .AddType(SchemaTypeConfigBuilder()
+                                            .SetType("db2/message")
+                                            .SetDatabase("db2/"))
+                               .Build();
+  SchemaProto expected_full_schema = SchemaBuilder()
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db1/email")
+                                                      .SetDatabase("db1/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db1/message")
+                                                      .SetDatabase("db1/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/email")
+                                                      .SetDatabase("db2/"))
+                                         .AddType(SchemaTypeConfigBuilder()
+                                                      .SetType("db2/message")
+                                                      .SetDatabase("db2/"))
+                                         .Build();
   SchemaStore::SetSchemaResult result;
   result.success = true;
-  result.schema_types_new_by_name.insert("db1_email");
-  result.schema_types_new_by_name.insert("db1_message");
+  result.schema_types_new_by_name.insert("db1/email");
+  result.schema_types_new_by_name.insert("db1/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db1_schema, /*database=*/"db1",
+                  db1_schema, /*database=*/"db1/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   result = SchemaStore::SetSchemaResult();
   result.success = true;
-  result.schema_types_new_by_name.insert("db2_email");
-  result.schema_types_new_by_name.insert("db2_message");
+  result.schema_types_new_by_name.insert("db2/email");
+  result.schema_types_new_by_name.insert("db2/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db2_schema, /*database=*/"db2",
+                  db2_schema, /*database=*/"db2/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   ICING_ASSERT_OK_AND_ASSIGN(const SchemaProto* actual_full_schema,
@@ -1566,23 +2739,23 @@
           .Build();
   SchemaProto db2_schema_incompatible = SchemaBuilder()
                                             .AddType(SchemaTypeConfigBuilder()
-                                                         .SetType("db2_email")
-                                                         .SetDatabase("db2")
+                                                         .SetType("db2/email")
+                                                         .SetDatabase("db2/")
                                                          .AddProperty(prop)
                                                          .AddProperty(prop))
                                             .Build();
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
                   db2_schema_incompatible,
-                  /*database=*/"db2",
+                  /*database=*/"db2/",
                   /*ignore_errors_and_delete_documents=*/false)),
               StatusIs(libtextclassifier3::StatusCode::ALREADY_EXISTS));
 
   // Check the schema, this should not have changed
   EXPECT_THAT(schema_store->GetSchema(),
               IsOkAndHolds(Pointee(EqualsProto(expected_full_schema))));
-  EXPECT_THAT(schema_store->GetSchema("db1"),
+  EXPECT_THAT(schema_store->GetSchema("db1/"),
               IsOkAndHolds(EqualsProto(db1_schema)));
-  EXPECT_THAT(schema_store->GetSchema("db2"),
+  EXPECT_THAT(schema_store->GetSchema("db2/"),
               IsOkAndHolds(EqualsProto(db2_schema)));
 }
 
@@ -1593,21 +2766,22 @@
                           feature_flags_.get(),
                           /*initialize_stats=*/nullptr));
 
-  SchemaProto combined_schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .Build();
+  SchemaProto combined_schema = SchemaBuilder()
+                                    .AddType(SchemaTypeConfigBuilder()
+                                                 .SetType("db2/email")
+                                                 .SetDatabase("db2/"))
+                                    .AddType(SchemaTypeConfigBuilder()
+                                                 .SetType("db2/message")
+                                                 .SetDatabase("db2/"))
+                                    .AddType(SchemaTypeConfigBuilder()
+                                                 .SetType("db1/email")
+                                                 .SetDatabase("db1/"))
+                                    .AddType(SchemaTypeConfigBuilder()
+                                                 .SetType("db1/message")
+                                                 .SetDatabase("db1/"))
+                                    .Build();
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  combined_schema, /*database=*/"db1",
+                  combined_schema, /*database=*/"db1/",
                   /*ignore_errors_and_delete_documents=*/false)),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
@@ -1623,47 +2797,29 @@
       SchemaBuilder()
           // This type does not explicitly set its database, so it defaults to
           // the empty database.
-          .AddType(SchemaTypeConfigBuilder().SetType("db1_email"))
+          .AddType(SchemaTypeConfigBuilder().SetType("db1/email"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
+                       .SetType("db1/message")
+                       .SetDatabase("db1/"))
           .Build();
 
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  schema, /*database=*/"db1",
+                  schema, /*database=*/"db1/",
                   /*ignore_errors_and_delete_documents=*/false)),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
-  schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .Build();
+  schema = SchemaBuilder()
+               .AddType(SchemaTypeConfigBuilder()
+                            .SetType("db1/email")
+                            .SetDatabase("db1/"))
+               .AddType(SchemaTypeConfigBuilder()
+                            .SetType("db1/message")
+                            .SetDatabase("db1/"))
+               .Build();
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
                   schema, /*database=*/"db_mismatch",
                   /*ignore_errors_and_delete_documents=*/false)),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
-
-  schema =
-      SchemaBuilder()
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
-          .AddType(
-              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
-          .Build();
-  EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  schema, /*database=*/"",
-                  /*ignore_errors_and_delete_documents=*/false)),
-              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
 TEST_F(SchemaStoreTest, SetSchemaWithDuplicateTypeNameAcrossDifferentDbFails) {
@@ -1677,42 +2833,42 @@
   SchemaProto db1_schema =
       SchemaBuilder()
           .AddType(
-              SchemaTypeConfigBuilder().SetType("email").SetDatabase("db1"))
+              SchemaTypeConfigBuilder().SetType("email").SetDatabase("db1/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db1_message")
-                       .SetDatabase("db1"))
+                       .SetType("db1/message")
+                       .SetDatabase("db1/"))
           .Build();
   SchemaStore::SetSchemaResult result;
   result.success = true;
   result.schema_types_new_by_name.insert("email");
-  result.schema_types_new_by_name.insert("db1_message");
+  result.schema_types_new_by_name.insert("db1/message");
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db1_schema, /*database=*/"db1",
+                  db1_schema, /*database=*/"db1/",
                   /*ignore_errors_and_delete_documents=*/false)),
               IsOkAndHolds(EqualsSetSchemaResult(result)));
   EXPECT_THAT(schema_store->GetSchema(),
               IsOkAndHolds(Pointee(EqualsProto(db1_schema))));
-  EXPECT_THAT(schema_store->GetSchema("db1"),
+  EXPECT_THAT(schema_store->GetSchema("db1/"),
               IsOkAndHolds(EqualsProto(db1_schema)));
 
   // Set schema in db2 with the same type name
   SchemaProto db2_schema =
       SchemaBuilder()
           .AddType(
-              SchemaTypeConfigBuilder().SetType("email").SetDatabase("db2"))
+              SchemaTypeConfigBuilder().SetType("email").SetDatabase("db2/"))
           .AddType(SchemaTypeConfigBuilder()
-                       .SetType("db2_message")
-                       .SetDatabase("db2"))
+                       .SetType("db2/message")
+                       .SetDatabase("db2/"))
           .Build();
   EXPECT_THAT(schema_store->SetSchema(CreateSetSchemaRequestProto(
-                  db2_schema, /*database=*/"db2",
+                  db2_schema, /*database=*/"db2/",
                   /*ignore_errors_and_delete_documents=*/false)),
               StatusIs(libtextclassifier3::StatusCode::ALREADY_EXISTS));
 
   // Check schema, this should not have changed
   EXPECT_THAT(schema_store->GetSchema(),
               IsOkAndHolds(Pointee(EqualsProto(db1_schema))));
-  EXPECT_THAT(schema_store->GetSchema("db1"),
+  EXPECT_THAT(schema_store->GetSchema("db1/"),
               IsOkAndHolds(EqualsProto(db1_schema)));
 }
 
@@ -1860,21 +3016,21 @@
     EXPECT_THAT(*actual_schema, EqualsProto(schema));
   } else {
     // Since we assign SchemaTypeIds based on order in the SchemaProto, this
-    // will
-    // cause SchemaTypeIds to change
+    // will cause SchemaTypeIds to change
     result = SchemaStore::SetSchemaResult();
     result.success = true;
-    result.old_schema_type_ids_changed.emplace(
-        0);  // Old SchemaTypeId of "email"
-    result.old_schema_type_ids_changed.emplace(
-        1);  // Old SchemaTypeId of "message"
+    // Old SchemaTypeId of "email"
+    result.old_schema_type_ids_changed.emplace(0);
+    // Old SchemaTypeId of "message"
+    result.old_schema_type_ids_changed.emplace(1);
 
     // Set the compatible schema
-    EXPECT_THAT(schema_store->SetSchema(
-                    schema, /*ignore_errors_and_delete_documents=*/false),
-                IsOkAndHolds(EqualsSetSchemaResult(result)));
+    EXPECT_THAT(
+        schema_store->SetSchema(reordered_schema,
+                                /*ignore_errors_and_delete_documents=*/false),
+        IsOkAndHolds(EqualsSetSchemaResult(result)));
     ICING_ASSERT_OK_AND_ASSIGN(actual_schema, schema_store->GetSchema());
-    EXPECT_THAT(*actual_schema, EqualsProto(schema));
+    EXPECT_THAT(*actual_schema, EqualsProto(reordered_schema));
   }
 }
 
@@ -2427,8 +3583,8 @@
   new_expected_result.success = true;
   new_expected_result.schema_types_scorable_property_inconsistent_by_id.insert(
       0);
-  new_expected_result.schema_types_changed_fully_compatible_by_name.insert(
-      "email");
+  new_expected_result.schema_types_scorable_property_inconsistent_by_name
+      .insert("email");
   EXPECT_THAT(schema_store->SetSchema(
                   new_schema, /*ignore_errors_and_delete_documents=*/false),
               IsOkAndHolds(EqualsSetSchemaResult(new_expected_result)));
@@ -2437,7 +3593,7 @@
 }
 
 TEST_F(SchemaStoreTest,
-       SetSchemaWithReorderedSchemeTypesAndUpdatedScorablePropertyOk) {
+       SetSchemaWithChangedSchemaTypeIdAndUpdatedScorablePropertyOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
       SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
@@ -2459,7 +3615,7 @@
                                         .SetCardinality(CARDINALITY_OPTIONAL)))
           .Build();
   // The new schema updates "score" as scorable_type ENABLED from type "email",
-  // and it also reorders the schema types of "email" and "message".
+  // and it also changes the schema types of "email".
   SchemaProto new_schema =
       SchemaBuilder()
           .AddType(SchemaTypeConfigBuilder()
@@ -2473,7 +3629,6 @@
                                         .SetDataType(TYPE_DOUBLE)
                                         .SetScorableType(SCORABLE_TYPE_ENABLED)
                                         .SetCardinality(CARDINALITY_OPTIONAL)))
-          .AddType(SchemaTypeConfigBuilder().SetType("message"))
           .Build();
 
   // Set old schema
@@ -2495,12 +3650,14 @@
   SchemaTypeId email_schema_type_id = 0;
   new_expected_result.schema_types_scorable_property_inconsistent_by_id.insert(
       email_schema_type_id);
-  new_expected_result.schema_types_changed_fully_compatible_by_name.insert(
-      "email");
-  new_expected_result.old_schema_type_ids_changed.insert(0);
+  new_expected_result.schema_types_scorable_property_inconsistent_by_name
+      .insert("email");
+  // 'email' schema type id is changed. 'message' is deleted.
   new_expected_result.old_schema_type_ids_changed.insert(1);
+  new_expected_result.schema_types_deleted_by_id.insert(0);
+  new_expected_result.schema_types_deleted_by_name.insert("message");
   EXPECT_THAT(schema_store->SetSchema(
-                  new_schema, /*ignore_errors_and_delete_documents=*/false),
+                  new_schema, /*ignore_errors_and_delete_documents=*/true),
               IsOkAndHolds(EqualsSetSchemaResult(new_expected_result)));
   ICING_ASSERT_OK_AND_ASSIGN(actual_schema, schema_store->GetSchema());
   EXPECT_THAT(*actual_schema, EqualsProto(new_schema));
@@ -4928,6 +6085,22 @@
                   EqualsScorablePropertyInfo("scoreInt", TYPE_INT64)))));
 }
 
+TEST_F(SchemaStoreTest, GetSchemaNameHash) {
+  // Hardcode some inputs to the GetSchemaNameHash function, so that we can be
+  // aware of any changes to the hashing function.
+  EXPECT_EQ(SchemaStore::GetSchemaNameHash("schema1"), 2607648336);
+  EXPECT_EQ(SchemaStore::GetSchemaNameHash("schema2"), 40165354);
+  EXPECT_EQ(SchemaStore::GetSchemaNameHash("schema3"), 1969483644);
+  EXPECT_EQ(SchemaStore::GetSchemaNameHash("aa"), 1179847464);
+  EXPECT_EQ(SchemaStore::GetSchemaNameHash("bb"), 4101441873);
+  EXPECT_EQ(SchemaStore::GetSchemaNameHash("abc"), 3395655888);
+  EXPECT_EQ(SchemaStore::GetSchemaNameHash("cba"), 669986194);
+  EXPECT_EQ(SchemaStore::GetSchemaNameHash("aab"), 2521824133);
+  EXPECT_EQ(SchemaStore::GetSchemaNameHash("bba"), 640501669);
+  EXPECT_EQ(SchemaStore::GetSchemaNameHash("1234"), 3131523007);
+  EXPECT_EQ(SchemaStore::GetSchemaNameHash("4321"), 3855245428);
+}
+
 class SchemaStoreTestWithParam
     : public SchemaStoreTest,
       public testing::WithParamInterface<version_util::StateChange> {};
@@ -4967,7 +6140,7 @@
   SchemaTypeConfigProto db1_email_rfc =
       SchemaTypeConfigBuilder()
           .SetType("db1/email")
-          .SetDatabase("db1")
+          .SetDatabase("db1/")
           .AddProperty(
               PropertyConfigBuilder()
                   .SetName("db1Subject")
@@ -4977,7 +6150,7 @@
   SchemaTypeConfigProto db1_email_no_rfc =
       SchemaTypeConfigBuilder()
           .SetType("db1/email")
-          .SetDatabase("db1")
+          .SetDatabase("db1/")
           .AddProperty(PropertyConfigBuilder()
                            .SetName("db1Subject")
                            .SetCardinality(CARDINALITY_OPTIONAL)
@@ -4986,7 +6159,7 @@
   SchemaTypeConfigProto db2_email =
       SchemaTypeConfigBuilder()
           .SetType("db2/email")
-          .SetDatabase("db2")
+          .SetDatabase("db2/")
           .AddProperty(PropertyConfigBuilder()
                            .SetName("db2Subject")
                            .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
@@ -5019,41 +6192,41 @@
       EXPECT_THAT(
           schema_store->GetSchema(),
           IsOkAndHolds(Pointee(EqualsProto(full_schema_with_database_no_rfc))));
-      EXPECT_THAT(schema_store->GetSchema("db1"),
+      EXPECT_THAT(schema_store->GetSchema("db1/"),
                   IsOkAndHolds(EqualsProto(db1_schema_no_rfc)));
-      EXPECT_THAT(schema_store->GetSchema("db2"),
+      EXPECT_THAT(schema_store->GetSchema("db2/"),
                   IsOkAndHolds(EqualsProto(db2_schema)));
     } else {
       EXPECT_THAT(
           schema_store->GetSchema(),
           IsOkAndHolds(Pointee(EqualsProto(full_schema_with_database_rfc))));
-      EXPECT_THAT(schema_store->GetSchema("db1"),
+      EXPECT_THAT(schema_store->GetSchema("db1/"),
                   IsOkAndHolds(EqualsProto(db1_schema_rfc)));
-      EXPECT_THAT(schema_store->GetSchema("db2"),
+      EXPECT_THAT(schema_store->GetSchema("db2/"),
                   IsOkAndHolds(EqualsProto(db2_schema)));
 
       DocumentProto db1_email_doc =
           DocumentBuilder()
               .SetKey("namespace", "uri1")
               .SetSchema("db1/email")
-              .AddStringProperty("db1Subject", "db1_subject")
+              .AddStringProperty("db1Subject", "db1/subject")
               .Build();
       DocumentProto db2_email_doc =
           DocumentBuilder()
               .SetKey("namespace", "uri3")
               .SetSchema("db2/email")
-              .AddStringProperty("db2Subject", "db2_subject")
+              .AddStringProperty("db2Subject", "db2/subject")
               .Build();
 
       // Verify that our in-memory structures are ok
       ICING_ASSERT_OK_AND_ASSIGN(SectionGroup section_group,
                                  schema_store->ExtractSections(db1_email_doc));
       EXPECT_THAT(section_group.string_sections[0].content,
-                  ElementsAre("db1_subject"));
+                  ElementsAre("db1/subject"));
       ICING_ASSERT_OK_AND_ASSIGN(section_group,
                                  schema_store->ExtractSections(db2_email_doc));
       EXPECT_THAT(section_group.string_sections[0].content,
-                  ElementsAre("db2_subject"));
+                  ElementsAre("db2/subject"));
 
       // Verify that our persisted data are ok
       EXPECT_THAT(schema_store->GetSchemaTypeId("db1/email"), IsOkAndHolds(0));
diff --git a/icing/schema/schema-util.cc b/icing/schema/schema-util.cc
index cfc2391..b482e15 100644
--- a/icing/schema/schema-util.cc
+++ b/icing/schema/schema-util.cc
@@ -49,8 +49,30 @@
 
 bool AreDocumentIndexingConfigsEqual(const DocumentIndexingConfig& old_config,
                                      const DocumentIndexingConfig& new_config) {
-  return old_config.index_nested_properties() ==
-         new_config.index_nested_properties();
+  // TODO(b/265304217): This could mark the new schema as incompatible and
+  // generate some unnecessary index rebuilds if the two schemas have an
+  // equivalent set of indexed properties, but changed the way that it is
+  // declared.
+  if (old_config.index_nested_properties() !=
+      new_config.index_nested_properties()) {
+    return false;
+  }
+
+  if (old_config.indexable_nested_properties_list().size() !=
+      new_config.indexable_nested_properties_list().size()) {
+    return false;
+  }
+
+  std::unordered_set<std::string_view> old_indexable_nested_properies_set(
+      old_config.indexable_nested_properties_list().begin(),
+      old_config.indexable_nested_properties_list().end());
+  for (const auto& property : new_config.indexable_nested_properties_list()) {
+    if (old_indexable_nested_properies_set.find(property) ==
+        old_indexable_nested_properies_set.end()) {
+      return false;
+    }
+  }
+  return true;
 }
 
 bool AreIntegerIndexingConfigsEqual(const IntegerIndexingConfig& old_config,
@@ -100,7 +122,7 @@
   if (old_property.cardinality() < new_property.cardinality()) {
     // We allow a new, less restrictive cardinality (i.e. a REQUIRED field
     // can become REPEATED or OPTIONAL, but not the other way around).
-    ICING_VLOG(1) << absl_ports::StrCat(
+    ICING_LOG(INFO) << absl_ports::StrCat(
         "Cardinality is more restrictive than before ",
         PropertyConfigProto::Cardinality::Code_Name(old_property.cardinality()),
         "->",
@@ -117,7 +139,7 @@
     // TODO(cassiewang): Maybe we can be a bit looser with this, e.g. we just
     // string cast an int64_t to a string. But for now, we'll stick with
     // simplistics.
-    ICING_VLOG(1) << absl_ports::StrCat(
+    ICING_LOG(INFO) << absl_ports::StrCat(
         "Data type ",
         PropertyConfigProto::DataType::Code_Name(old_property.data_type()),
         "->",
@@ -130,9 +152,9 @@
 bool IsSchemaTypeCompatible(const PropertyConfigProto& old_property,
                             const PropertyConfigProto& new_property) {
   if (old_property.schema_type() != new_property.schema_type()) {
-    ICING_VLOG(1) << absl_ports::StrCat("Schema type ",
-                                        old_property.schema_type(), "->",
-                                        new_property.schema_type());
+    ICING_LOG(INFO) << absl_ports::StrCat("Schema type ",
+                                          old_property.schema_type(), "->",
+                                          new_property.schema_type());
     return false;
   }
   return true;
@@ -145,53 +167,6 @@
          IsCardinalityCompatible(old_property, new_property);
 }
 
-bool IsTermMatchTypeCompatible(const StringIndexingConfig& old_indexed,
-                               const StringIndexingConfig& new_indexed) {
-  return old_indexed.term_match_type() == new_indexed.term_match_type() &&
-         old_indexed.tokenizer_type() == new_indexed.tokenizer_type();
-}
-
-bool IsIntegerNumericMatchTypeCompatible(
-    const IntegerIndexingConfig& old_indexed,
-    const IntegerIndexingConfig& new_indexed) {
-  return old_indexed.numeric_match_type() == new_indexed.numeric_match_type();
-}
-
-bool IsEmbeddingIndexingCompatible(const EmbeddingIndexingConfig& old_indexed,
-                                   const EmbeddingIndexingConfig& new_indexed) {
-  return old_indexed.embedding_indexing_type() ==
-             new_indexed.embedding_indexing_type() &&
-         old_indexed.quantization_type() == new_indexed.quantization_type();
-}
-
-bool IsDocumentIndexingCompatible(const DocumentIndexingConfig& old_indexed,
-                                  const DocumentIndexingConfig& new_indexed) {
-  // TODO(b/265304217): This could mark the new schema as incompatible and
-  // generate some unnecessary index rebuilds if the two schemas have an
-  // equivalent set of indexed properties, but changed the way that it is
-  // declared.
-  if (old_indexed.index_nested_properties() !=
-      new_indexed.index_nested_properties()) {
-    return false;
-  }
-
-  if (old_indexed.indexable_nested_properties_list().size() !=
-      new_indexed.indexable_nested_properties_list().size()) {
-    return false;
-  }
-
-  std::unordered_set<std::string_view> old_indexable_nested_properies_set(
-      old_indexed.indexable_nested_properties_list().begin(),
-      old_indexed.indexable_nested_properties_list().end());
-  for (const auto& property : new_indexed.indexable_nested_properties_list()) {
-    if (old_indexable_nested_properies_set.find(property) ==
-        old_indexable_nested_properies_set.end()) {
-      return false;
-    }
-  }
-  return true;
-}
-
 void AddIncompatibleChangeToDelta(
     std::unordered_set<std::string>& incompatible_delta,
     const SchemaTypeConfigProto& old_type_config,
@@ -1123,10 +1098,12 @@
 
   // TODO(cassiewang): consider caching property_config_map for some properties,
   // e.g. using LRU cache. Or changing schema.proto to use go/protomap.
-  for (const PropertyConfigProto& property_config : type_config.properties()) {
+  for (int position = 0; position < type_config.properties_size(); ++position) {
+    const PropertyConfigProto& property_config =
+        type_config.properties(position);
     std::string_view property_name = property_config.property_name();
-    parsed_property_configs.property_config_map.emplace(property_name,
-                                                        &property_config);
+    parsed_property_configs.property_config_map.emplace(
+        property_name, PropertyConfigInfo{&property_config, position});
     if (property_config.cardinality() ==
         PropertyConfigProto::Cardinality::REQUIRED) {
       parsed_property_configs.required_properties.insert(property_name);
@@ -1206,7 +1183,10 @@
     bool is_incompatible = false;
     bool is_index_incompatible = false;
     bool is_join_incompatible = false;
-    for (const auto& old_property_config : old_type_config.properties()) {
+    for (int position = 0; position < old_type_config.properties_size();
+         ++position) {
+      const PropertyConfigProto& old_property_config =
+          old_type_config.properties(position);
       std::string_view property_name = old_property_config.property_name();
       if (old_property_config.cardinality() ==
           PropertyConfigProto::Cardinality::REQUIRED) {
@@ -1242,7 +1222,7 @@
       if (new_property_name_and_config ==
           new_parsed_property_configs.property_config_map.end()) {
         // Didn't find the old property
-        ICING_VLOG(1) << absl_ports::StrCat(
+        ICING_LOG(INFO) << absl_ports::StrCat(
             "Previously defined property type '", old_type_config.schema_type(),
             ".", old_property_config.property_name(),
             "' was not defined in new schema");
@@ -1254,38 +1234,45 @@
       }
 
       const PropertyConfigProto* new_property_config =
-          new_property_name_and_config->second;
+          new_property_name_and_config->second.property_config;
+      bool property_order_changed =
+          feature_flags.enable_schema_definition_deduping() &&
+          position != new_property_name_and_config->second.position;
       if (!has_property_changed &&
-          !ArePropertiesEqual(old_property_config, *new_property_config)) {
-        // Finally found a property that changed.
+          (!ArePropertiesEqual(old_property_config, *new_property_config) ||
+           property_order_changed)) {
+        // Found a property that changed. A property change is either a
+        // PropertyConfigProto change or (when schema deduping is enabled) a
+        // change in the property's position in the type config's repeated
+        // properties field.
         has_property_changed = true;
       }
 
       if (!IsPropertyCompatible(old_property_config, *new_property_config)) {
-        ICING_VLOG(1) << absl_ports::StrCat(
+        ICING_LOG(INFO) << absl_ports::StrCat(
             "Property '", old_type_config.schema_type(), ".",
             old_property_config.property_name(), "' is incompatible.");
         is_incompatible = true;
       }
 
       // Any change in the indexed property requires a reindexing
-      if (!IsTermMatchTypeCompatible(
+      if (!AreStringIndexingConfigsEqual(
               old_property_config.string_indexing_config(),
               new_property_config->string_indexing_config()) ||
-          !IsIntegerNumericMatchTypeCompatible(
+          !AreIntegerIndexingConfigsEqual(
               old_property_config.integer_indexing_config(),
               new_property_config->integer_indexing_config()) ||
-          !IsDocumentIndexingCompatible(
+          !AreDocumentIndexingConfigsEqual(
               old_property_config.document_indexing_config(),
               new_property_config->document_indexing_config()) ||
-          !IsEmbeddingIndexingCompatible(
+          !AreEmbeddingIndexingConfigsEqual(
               old_property_config.embedding_indexing_config(),
               new_property_config->embedding_indexing_config())) {
         is_index_incompatible = true;
       }
 
-      if (old_property_config.joinable_config().value_type() !=
-          new_property_config->joinable_config().value_type()) {
+      if (!AreJoinableConfigsEqual(old_property_config.joinable_config(),
+                                   new_property_config->joinable_config())) {
         is_join_incompatible = true;
       }
     }
@@ -1297,7 +1284,7 @@
     // here to detect new required properties.
     if (!IsSubset(new_parsed_property_configs.required_properties,
                   old_required_properties)) {
-      ICING_VLOG(1) << absl_ports::StrCat(
+      ICING_LOG(INFO) << absl_ports::StrCat(
           "New schema '", old_type_config.schema_type(),
           "' has REQUIRED properties that are not "
           "present in the previously defined schema");
@@ -1310,9 +1297,9 @@
     // reindex everything.
     if (!IsSubset(new_parsed_property_configs.indexed_properties,
                   old_indexed_properties)) {
-      ICING_VLOG(1) << "Set of indexed properties in schema type '"
-                    << old_type_config.schema_type()
-                    << "' has changed, required reindexing.";
+      ICING_LOG(INFO) << "Set of indexed properties in schema type '"
+                      << old_type_config.schema_type()
+                      << "' has changed, required reindexing.";
       is_index_incompatible = true;
     }
 
@@ -1327,9 +1314,10 @@
                   old_joinable_properties) ||
         !IsSubset(new_parsed_property_configs.nested_document_properties,
                   old_nested_document_properties)) {
-      ICING_VLOG(1) << "Set of joinable properties in schema type '"
-                    << old_type_config.schema_type()
-                    << "' has changed, required reconstructing joinable cache.";
+      ICING_LOG(INFO)
+          << "Set of joinable properties in schema type '"
+          << old_type_config.schema_type()
+          << "' has changed, required reconstructing joinable cache.";
       is_join_incompatible = true;
     }
 
@@ -1351,8 +1339,16 @@
                                    old_type_config_map, new_type_config_map);
     }
 
+    // Scorable-property inconsistent types are already added to the schema
+    // delta in FindScorablePropertyInconsistentTypes above.
+    bool is_scorable_property_cache_incompatible =
+        !schema_delta.schema_types_scorable_property_inconsistent.empty() &&
+        schema_delta.schema_types_scorable_property_inconsistent.find(
+            old_type_config.schema_type()) !=
+            schema_delta.schema_types_scorable_property_inconsistent.end();
+
     if (!is_incompatible && !is_index_incompatible && !is_join_incompatible &&
-        has_property_changed) {
+        !is_scorable_property_cache_incompatible && has_property_changed) {
       schema_delta.schema_types_changed_fully_compatible.insert(
           old_type_config.schema_type());
     }
diff --git a/icing/schema/schema-util.h b/icing/schema/schema-util.h
index 39e6160..fa2e575 100644
--- a/icing/schema/schema-util.h
+++ b/icing/schema/schema-util.h
@@ -15,6 +15,7 @@
 #ifndef ICING_SCHEMA_SCHEMA_UTIL_H_
 #define ICING_SCHEMA_SCHEMA_UTIL_H_
 
+#include <cstdint>
 #include <string>
 #include <string_view>
 #include <unordered_map>
@@ -116,9 +117,19 @@
     }
   };
 
+  // A struct that stores the information about a property config parsed from
+  // a SchemaTypeConfigProto.
+  struct PropertyConfigInfo {
+    const PropertyConfigProto* property_config;
+
+    // The position of the property in the type config's repeated property
+    // field.
+    int32_t position;
+  };
+
   struct ParsedPropertyConfigs {
     // Mapping of property name to PropertyConfigProto
-    std::unordered_map<std::string_view, const PropertyConfigProto*>
+    std::unordered_map<std::string_view, PropertyConfigInfo>
         property_config_map;
 
     // Properties that have an indexing config
diff --git a/icing/schema/schema-util_test.cc b/icing/schema/schema-util_test.cc
index 6758984..745c7dd 100644
--- a/icing/schema/schema-util_test.cc
+++ b/icing/schema/schema-util_test.cc
@@ -2438,6 +2438,55 @@
               Eq(schema_delta));
 }
 
+TEST_P(SchemaUtilTest, PropertyConfigReorderingIsCompatible) {
+  // Configure old schema
+  SchemaProto old_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType(kEmailType)
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("prop1")
+                                        .SetDataType(TYPE_STRING)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("prop2")
+                                        .SetDataType(TYPE_INT64)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("prop3")
+                                        .SetDataType(TYPE_STRING)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  // Configure new schema with the properties in a different order
+  SchemaProto new_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType(kEmailType)
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("prop2")
+                                        .SetDataType(TYPE_INT64)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("prop1")
+                                        .SetDataType(TYPE_STRING)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("prop3")
+                                        .SetDataType(TYPE_STRING)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  SchemaUtil::SchemaDelta schema_delta;
+  if (feature_flags_->enable_schema_definition_deduping()) {
+    schema_delta.schema_types_changed_fully_compatible.insert(kEmailType);
+  }
+  SchemaUtil::DependentMap no_dependents_map;
+  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
+                  old_schema, new_schema, no_dependents_map, *feature_flags_),
+              Eq(schema_delta));
+}
+
 TEST_P(SchemaUtilTest, CompatibilityOfDifferentCardinalityOk) {
   // Configure less restrictive schema based on cardinality
   SchemaProto less_restrictive_schema =
@@ -3617,9 +3666,10 @@
               IsEmpty());
 }
 
-TEST_P(SchemaUtilTest, ChangingJoinablePropertiesPropagateDeleteIsCompatible) {
+TEST_P(SchemaUtilTest,
+       ChangingJoinablePropertiesPropagateDeleteIsJoinIncompatible) {
   // Configure old schema
-  SchemaProto old_schema =
+  SchemaProto old_schema_without_delete_propagation =
       SchemaBuilder()
           .AddType(SchemaTypeConfigBuilder().SetType("MyType").AddProperty(
               PropertyConfigBuilder()
@@ -3631,7 +3681,7 @@
           .Build();
 
   // Configure new schema with delete propagation type PROPAGATE_FROM
-  SchemaProto new_schema_with_optional =
+  SchemaProto new_schema_with_delete_propagation =
       SchemaBuilder()
           .AddType(SchemaTypeConfigBuilder().SetType("MyType").AddProperty(
               PropertyConfigBuilder()
@@ -3642,13 +3692,23 @@
                   .SetCardinality(CARDINALITY_REQUIRED)))
           .Build();
 
-  SchemaUtil::SchemaDelta schema_delta;
-  schema_delta.schema_types_changed_fully_compatible.insert("MyType");
+  SchemaUtil::SchemaDelta expected_schema_delta;
+  expected_schema_delta.schema_types_join_incompatible.insert("MyType");
+
+  // New schema enabled delete propagation.
   SchemaUtil::DependentMap no_dependents_map;
   EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
-                  old_schema, new_schema_with_optional, no_dependents_map,
+                  old_schema_without_delete_propagation,
+                  new_schema_with_delete_propagation, no_dependents_map,
                   *feature_flags_),
-              Eq(schema_delta));
+              Eq(expected_schema_delta));
+
+  // New schema disabled a joinable property.
+  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
+                  old_schema_without_delete_propagation,
+                  new_schema_with_delete_propagation, no_dependents_map,
+                  *feature_flags_),
+              Eq(expected_schema_delta));
 }
 
 TEST_P(SchemaUtilTest, AddingTypeIsCompatible) {
@@ -4436,7 +4496,21 @@
       /*enable_repeated_field_joins=*/false,
       /*enable_embedding_backup_generation=*/true,
       /*enable_schema_database=*/true,
-      /*release_backup_schema_file_if_overlay_present=*/true);
+      /*release_backup_schema_file_if_overlay_present=*/true,
+      /*enable_strict_page_byte_size_limit=*/true,
+      /*enable_smaller_decompression_buffer_size=*/true,
+      /*enable_eigen_embedding_scoring=*/true,
+      /*enable_passing_filter_to_children=*/true,
+      /*enable_proto_log_new_header_format=*/true,
+      /*enable_embedding_iterator_v2=*/true,
+      /*enable_reusable_decompression_buffer=*/true,
+      /*enable_schema_type_id_optimization=*/true,
+      /*enable_optimize_improvements=*/true,
+      /*expired_document_purge_threshold_ms=*/0,
+      /*enable_non_existent_qualified_id_join=*/true,
+      /*enable_skip_set_schema_type_equality_check=*/true,
+      /*enable_embed_query_optimization=*/true,
+      /*enable_schema_definition_deduping=*/true);
   SchemaProto schema =
       SchemaBuilder()
           .AddType(SchemaTypeConfigBuilder().SetType("MyType").AddProperty(
@@ -4498,7 +4572,21 @@
       /*enable_repeated_field_joins=*/true,
       /*enable_embedding_backup_generation=*/true,
       /*enable_schema_database=*/true,
-      /*release_backup_schema_file_if_overlay_present=*/true);
+      /*release_backup_schema_file_if_overlay_present=*/true,
+      /*enable_strict_page_byte_size_limit=*/true,
+      /*enable_smaller_decompression_buffer_size=*/true,
+      /*enable_eigen_embedding_scoring=*/true,
+      /*enable_passing_filter_to_children=*/true,
+      /*enable_proto_log_new_header_format=*/true,
+      /*enable_embedding_iterator_v2=*/true,
+      /*enable_reusable_decompression_buffer=*/true,
+      /*enable_schema_type_id_optimization=*/true,
+      /*enable_optimize_improvements=*/true,
+      /*expired_document_purge_threshold_ms=*/0,
+      /*enable_non_existent_qualified_id_join=*/true,
+      /*enable_skip_set_schema_type_equality_check=*/true,
+      /*enable_embed_query_optimization=*/true,
+      /*enable_schema_definition_deduping=*/true);
 
   SchemaProto schema =
       SchemaBuilder()
@@ -5818,23 +5906,50 @@
 
 INSTANTIATE_TEST_SUITE_P(
     SchemaUtilTest, SchemaUtilTest,
-    testing::Values(
-        FeatureFlags(
-            /*enable_circular_schema_definitions=*/false,
-            /*enable_scorable_properties=*/true,
-            /*enable_embedding_quantization=*/true,
-            /*enable_repeated_field_joins=*/true,
-            /*enable_embedding_backup_generation=*/true,
-            /*enable_schema_database=*/true,
-            /*release_backup_schema_file_if_overlay_present=*/true),
-        FeatureFlags(
-            /*enable_circular_schema_definitions=*/true,
-            /*enable_scorable_properties=*/true,
-            /*enable_embedding_quantization=*/true,
-            /*enable_repeated_field_joins=*/true,
-            /*enable_embedding_backup_generation=*/true,
-            /*enable_schema_database=*/true,
-            /*release_backup_schema_file_if_overlay_present=*/true)));
+    testing::Values(FeatureFlags(
+                        /*enable_circular_schema_definitions=*/false,
+                        /*enable_scorable_properties=*/true,
+                        /*enable_embedding_quantization=*/true,
+                        /*enable_repeated_field_joins=*/true,
+                        /*enable_embedding_backup_generation=*/true,
+                        /*enable_schema_database=*/true,
+                        /*release_backup_schema_file_if_overlay_present=*/true,
+                        /*enable_strict_page_byte_size_limit=*/true,
+                        /*enable_smaller_decompression_buffer_size=*/true,
+                        /*enable_eigen_embedding_scoring=*/true,
+                        /*enable_passing_filter_to_children=*/true,
+                        /*enable_proto_log_new_header_format=*/true,
+                        /*enable_embedding_iterator_v2=*/true,
+                        /*enable_reusable_decompression_buffer=*/true,
+                        /*enable_schema_type_id_optimization=*/true,
+                        /*enable_optimize_improvements=*/true,
+                        /*expired_document_purge_threshold_ms=*/0,
+                        /*enable_non_existent_qualified_id_join=*/true,
+                        /*enable_skip_set_schema_type_equality_check=*/true,
+                        /*enable_embed_query_optimization=*/true,
+                        /*enable_schema_definition_deduping=*/false),
+                    FeatureFlags(
+                        /*enable_circular_schema_definitions=*/true,
+                        /*enable_scorable_properties=*/true,
+                        /*enable_embedding_quantization=*/true,
+                        /*enable_repeated_field_joins=*/true,
+                        /*enable_embedding_backup_generation=*/true,
+                        /*enable_schema_database=*/true,
+                        /*release_backup_schema_file_if_overlay_present=*/true,
+                        /*enable_strict_page_byte_size_limit=*/true,
+                        /*enable_smaller_decompression_buffer_size=*/true,
+                        /*enable_eigen_embedding_scoring=*/true,
+                        /*enable_passing_filter_to_children=*/true,
+                        /*enable_proto_log_new_header_format=*/true,
+                        /*enable_embedding_iterator_v2=*/true,
+                        /*enable_reusable_decompression_buffer=*/true,
+                        /*enable_schema_type_id_optimization=*/true,
+                        /*enable_optimize_improvements=*/true,
+                        /*expired_document_purge_threshold_ms=*/0,
+                        /*enable_non_existent_qualified_id_join=*/true,
+                        /*enable_skip_set_schema_type_equality_check=*/true,
+                        /*enable_embed_query_optimization=*/true,
+                        /*enable_schema_definition_deduping=*/true)));
 
 struct IsIndexedPropertyTestParam {
   PropertyConfigProto property_config;
diff --git a/icing/scoring/advanced_scoring/advanced-scorer_fuzz_test.cc b/icing/scoring/advanced_scoring/advanced-scorer_fuzz_test.cc
index 765ccdb..c02f06b 100644
--- a/icing/scoring/advanced_scoring/advanced-scorer_fuzz_test.cc
+++ b/icing/scoring/advanced_scoring/advanced-scorer_fuzz_test.cc
@@ -22,6 +22,7 @@
 #include "icing/file/filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/index/embed/embedding-query-results.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/schema/schema-store.h"
 #include "icing/scoring/advanced_scoring/advanced-scorer.h"
 #include "icing/store/document-store.h"
@@ -55,6 +56,9 @@
           /*force_recovery_and_revalidate_documents=*/false,
           /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
           PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
+          PortableFileBackedProtoLog<
+              DocumentWrapper>::kDefaultCompressionThresholdBytes,
+          protobuf_ports::kDefaultMemLevel,
           /*initialize_stats=*/nullptr)
           .ValueOrDie()
           .document_store;
diff --git a/icing/scoring/advanced_scoring/advanced-scorer_test.cc b/icing/scoring/advanced_scoring/advanced-scorer_test.cc
index 386d0c2..a1eb58a 100644
--- a/icing/scoring/advanced_scoring/advanced-scorer_test.cc
+++ b/icing/scoring/advanced_scoring/advanced-scorer_test.cc
@@ -35,6 +35,7 @@
 #include "icing/index/embed/embedding-query-results.h"
 #include "icing/index/hit/doc-hit-info.h"
 #include "icing/join/join-children-fetcher-impl-deprecated.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/proto/scoring.pb.h"
@@ -48,9 +49,11 @@
 #include "icing/store/document-id.h"
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
+#include "icing/testing/embedding-test-utils.h"
 #include "icing/testing/fake-clock.h"
 #include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/document-util.h"
 
 namespace icing {
 namespace lib {
@@ -80,14 +83,18 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, doc_store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, doc_store_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
 
     // Creates the schema
@@ -286,7 +293,8 @@
 TEST_F(AdvancedScorerTest, SimpleExpression) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(CreateDocument("namespace", "uri")));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          CreateDocument("namespace", "uri"))));
   DocumentId document_id = put_result.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -307,7 +315,8 @@
 TEST_F(AdvancedScorerTest, BasicPureArithmeticExpression) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(CreateDocument("namespace", "uri")));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          CreateDocument("namespace", "uri"))));
   DocumentId document_id = put_result.new_document_id;
   DocHitInfo docHitInfo = DocHitInfo(document_id);
 
@@ -376,7 +385,8 @@
 TEST_F(AdvancedScorerTest, BasicMathFunctionExpression) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(CreateDocument("namespace", "uri")));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          CreateDocument("namespace", "uri"))));
   DocumentId document_id = put_result.new_document_id;
   DocHitInfo docHitInfo = DocHitInfo(document_id);
 
@@ -515,9 +525,9 @@
 TEST_F(AdvancedScorerTest, DocumentScoreCreationTimestampFunctionExpression) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(CreateDocument(
+      document_store_->Put(document_util::CreateDocumentWrapper(CreateDocument(
           "namespace", "uri", /*score=*/123,
-          /*creation_timestamp_ms=*/kDefaultCreationTimestampMs)));
+          /*creation_timestamp_ms=*/kDefaultCreationTimestampMs))));
   DocumentId document_id = put_result.new_document_id;
   DocHitInfo docHitInfo = DocHitInfo(document_id);
 
@@ -558,7 +568,8 @@
 TEST_F(AdvancedScorerTest, DocumentUsageFunctionExpression) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(CreateDocument("namespace", "uri")));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          CreateDocument("namespace", "uri"))));
   DocumentId document_id = put_result.new_document_id;
   DocHitInfo docHitInfo = DocHitInfo(document_id);
 
@@ -615,7 +626,8 @@
 TEST_F(AdvancedScorerTest, DocumentUsageFunctionOutOfRange) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(CreateDocument("namespace", "uri")));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          CreateDocument("namespace", "uri"))));
   DocumentId document_id = put_result.new_document_id;
   DocHitInfo docHitInfo = DocHitInfo(document_id);
 
@@ -667,8 +679,10 @@
           .SetCreationTimestampMs(kDefaultCreationTimestampMs)
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(test_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(test_document)));
   DocumentId document_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<AdvancedScorer> scorer,
@@ -691,17 +705,20 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store_->Put(CreateDocument("namespace", "uri1")));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          CreateDocument("namespace", "uri1"))));
   DocumentId document_id_1 = put_result1.new_document_id;
   DocHitInfo docHitInfo1 = DocHitInfo(document_id_1);
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
-      document_store_->Put(CreateDocument("namespace", "uri2")));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          CreateDocument("namespace", "uri2"))));
   DocumentId document_id_2 = put_result2.new_document_id;
   DocHitInfo docHitInfo2 = DocHitInfo(document_id_2);
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
-      document_store_->Put(CreateDocument("namespace", "uri3")));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          CreateDocument("namespace", "uri3"))));
   DocumentId document_id_3 = put_result3.new_document_id;
   DocHitInfo docHitInfo3 = DocHitInfo(document_id_3);
 
@@ -803,14 +820,20 @@
   DocumentProto test_document_3 =
       DocumentBuilder().SetKey("namespace", "uri3").SetSchema("person").Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(test_document_1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(test_document_1)));
   DocumentId document_id_1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(test_document_2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(test_document_2)));
   DocumentId document_id_2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store_->Put(test_document_3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(test_document_3)));
   DocumentId document_id_3 = put_result3.new_document_id;
 
   ScoringSpecProto spec_proto = CreateAdvancedScoringSpec("");
@@ -900,11 +923,15 @@
   DocumentProto test_document_2 =
       DocumentBuilder().SetKey("namespace", "uri2").SetSchema("person").Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(test_document_1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(test_document_1)));
   DocumentId document_id_1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(test_document_2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(test_document_2)));
   DocumentId document_id_2 = put_result2.new_document_id;
 
   ScoringSpecProto spec_proto = CreateAdvancedScoringSpec("");
@@ -1012,8 +1039,8 @@
   const int64_t creation_timestamp_ms = 123;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(CreateDocument("namespace", "uri", /*score=*/123,
-                                          creation_timestamp_ms)));
+      document_store_->Put(document_util::CreateDocumentWrapper(CreateDocument(
+          "namespace", "uri", /*score=*/123, creation_timestamp_ms))));
   DocumentId document_id = put_result.new_document_id;
   DocHitInfo docHitInfo = DocHitInfo(document_id);
 
@@ -1133,7 +1160,8 @@
   // non-constant 0.
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(CreateDocument("namespace", "uri", /*score=*/0)));
+      document_store_->Put(document_util::CreateDocumentWrapper(
+          CreateDocument("namespace", "uri", /*score=*/0))));
   DocumentId document_id = put_result.new_document_id;
   DocHitInfo docHitInfo = DocHitInfo(document_id);
 
@@ -1363,12 +1391,12 @@
 
 TEST_F(AdvancedScorerTest,
        MatchedSemanticScoresFunctionScoreExpressionTypeError) {
-  EmbeddingQueryResults embedding_query_results;
-  embedding_query_results
-      .result_infos[/*query_vector_index=*/0]
-                    [SearchSpecProto::EmbeddingQueryMetricType::COSINE]
-                    [/*document_id=*/0]
-      .AppendScore(/*semantic_score=*/0.1);
+  EmbeddingQueryResults embedding_query_results(/*num_query_vectors=*/1);
+  GetOrCreateEmbeddingMatchInfosForDocument(
+      embedding_query_results, /*query_vector_index=*/0,
+      SearchSpecProto::EmbeddingQueryMetricType::COSINE, /*document_id=*/0)
+      .AppendScore(*embedding_query_results.global_scores,
+                   /*semantic_score=*/0.1);
 
   libtextclassifier3::StatusOr<std::unique_ptr<AdvancedScorer>> scorer_or =
       AdvancedScorer::Create(
@@ -1464,17 +1492,17 @@
 
 TEST_F(AdvancedScorerTest,
        MatchedSemanticScoresFunctionScoreExpressionNotQueried) {
-  EmbeddingQueryResults embedding_query_results;
-  embedding_query_results
-      .result_infos[/*query_vector_index=*/0]
-                    [SearchSpecProto::EmbeddingQueryMetricType::COSINE]
-                    [/*document_id=*/0]
-      .AppendScore(/*semantic_score=*/0.1);
-  embedding_query_results
-      .result_infos[/*query_vector_index=*/1]
-                    [SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT]
-                    [/*document_id=*/1]
-      .AppendScore(/*semantic_score=*/0.2);
+  EmbeddingQueryResults embedding_query_results(/*num_query_vectors=*/2);
+  GetOrCreateEmbeddingMatchInfosForDocument(
+      embedding_query_results, /*query_vector_index=*/0,
+      SearchSpecProto::EmbeddingQueryMetricType::COSINE, /*document_id=*/0)
+      .AppendScore(*embedding_query_results.global_scores,
+                   /*semantic_score=*/0.1);
+  GetOrCreateEmbeddingMatchInfosForDocument(
+      embedding_query_results, /*query_vector_index=*/1,
+      SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT, /*document_id=*/1)
+      .AppendScore(*embedding_query_results.global_scores,
+                   /*semantic_score=*/0.2);
 
   libtextclassifier3::StatusOr<std::unique_ptr<AdvancedScorer>> scorer_or =
       AdvancedScorer::Create(CreateAdvancedScoringSpec(
@@ -1564,9 +1592,10 @@
 }
 
 void AddEntryToEmbeddingQueryScoreMap(
+    std::vector<double>& global_scores,
     EmbeddingQueryResults::EmbeddingQueryMatchInfoMap& score_map,
     double semantic_score, DocumentId document_id) {
-  score_map[document_id].AppendScore(semantic_score);
+  score_map[document_id].AppendScore(global_scores, semantic_score);
 }
 
 TEST_F(AdvancedScorerTest, MatchedSemanticScoresFunctionScoreExpression) {
@@ -1574,7 +1603,7 @@
   DocumentId document_id_1 = 1;
   DocHitInfo doc_hit_info_0(document_id_0);
   DocHitInfo doc_hit_info_1(document_id_1);
-  EmbeddingQueryResults embedding_query_results;
+  EmbeddingQueryResults embedding_query_results(/*num_query_vectors=*/2);
 
   // Let the first query assign the following semantic scores:
   // COSINE:
@@ -1586,41 +1615,57 @@
   // EUCLIDEAN:
   //   Document 0: 0.7
   //   Document 1: 0.8
-  EmbeddingQueryResults::EmbeddingQueryMatchInfoMap* score_map =
-      &embedding_query_results
-           .result_infos[0][SearchSpecProto::EmbeddingQueryMetricType::COSINE];
-  AddEntryToEmbeddingQueryScoreMap(*score_map,
+  ICING_ASSERT_OK_AND_ASSIGN(
+      EmbeddingQueryResults::EmbeddingQueryMatchInfoMap * score_map,
+      embedding_query_results.GetOrCreateMatchInfoMap(
+          /*query_vector_index=*/0,
+          SearchSpecProto::EmbeddingQueryMetricType::COSINE));
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
                                    /*semantic_score=*/0.1, document_id_0);
-  AddEntryToEmbeddingQueryScoreMap(*score_map,
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
                                    /*semantic_score=*/0.2, document_id_0);
-  AddEntryToEmbeddingQueryScoreMap(*score_map,
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
                                    /*semantic_score=*/0.3, document_id_1);
-  AddEntryToEmbeddingQueryScoreMap(*score_map,
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
                                    /*semantic_score=*/0.4, document_id_1);
-  score_map = &embedding_query_results.result_infos
-                   [0][SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT];
-  AddEntryToEmbeddingQueryScoreMap(*score_map,
+  ICING_ASSERT_OK_AND_ASSIGN(
+      score_map, embedding_query_results.GetOrCreateMatchInfoMap(
+                     /*query_vector_index=*/0,
+                     SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT));
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
                                    /*semantic_score=*/0.5, document_id_0);
-  AddEntryToEmbeddingQueryScoreMap(*score_map,
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
                                    /*semantic_score=*/0.6, document_id_1);
-  score_map =
-      &embedding_query_results
-           .result_infos[0]
-                         [SearchSpecProto::EmbeddingQueryMetricType::EUCLIDEAN];
-  AddEntryToEmbeddingQueryScoreMap(*score_map,
+  ICING_ASSERT_OK_AND_ASSIGN(
+      score_map, embedding_query_results.GetOrCreateMatchInfoMap(
+                     /*query_vector_index=*/0,
+                     SearchSpecProto::EmbeddingQueryMetricType::EUCLIDEAN));
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
                                    /*semantic_score=*/0.7, document_id_0);
-  AddEntryToEmbeddingQueryScoreMap(*score_map,
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
                                    /*semantic_score=*/0.8, document_id_1);
 
   // Let the second query only assign DOT_PRODUCT scores:
   // DOT_PRODUCT:
   //   Document 0: 0.1
   //   Document 1: 0.2
-  score_map = &embedding_query_results.result_infos
-                   [1][SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT];
-  AddEntryToEmbeddingQueryScoreMap(*score_map,
+  ICING_ASSERT_OK_AND_ASSIGN(
+      score_map, embedding_query_results.GetOrCreateMatchInfoMap(
+                     /*query_vector_index=*/1,
+                     SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT));
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
                                    /*semantic_score=*/0.1, document_id_0);
-  AddEntryToEmbeddingQueryScoreMap(*score_map,
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
                                    /*semantic_score=*/0.2, document_id_1);
 
   // Get semantic scores for default metric (DOT_PRODUCT) for the first query.
@@ -1708,6 +1753,75 @@
                         "has not been queried"));
 }
 
+// This test should be very uncommon, but we should still make sure it works.
+TEST_F(
+    AdvancedScorerTest,
+    MatchedSemanticScoresFunctionScoreExpression_NonConstantQueryVectorIndex) {
+  // Create document 0 with document score 0.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(CreateDocument(
+          "namespace", "uri0", /*score=*/0, kDefaultCreationTimestampMs))));
+  DocumentId document_id_0 = put_result.new_document_id;
+  DocHitInfo doc_hit_info_0(document_id_0);
+
+  // Create document 1 with document score 1.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(CreateDocument(
+          "namespace", "uri1", /*score=*/1, kDefaultCreationTimestampMs))));
+  DocumentId document_id_1 = put_result.new_document_id;
+  DocHitInfo doc_hit_info_1(document_id_1);
+
+  EmbeddingQueryResults embedding_query_results(/*num_query_vectors=*/2);
+
+  // Create the first embedding query with scores:
+  // - Document 0: 0.1
+  // - Document 1: 0.2
+  // Create the second embedding query with scores:
+  // - Document 0: -0.1
+  // - Document 1: -0.2
+  ICING_ASSERT_OK_AND_ASSIGN(
+      EmbeddingQueryResults::EmbeddingQueryMatchInfoMap * score_map,
+      embedding_query_results.GetOrCreateMatchInfoMap(
+          /*query_vector_index=*/0,
+          SearchSpecProto::EmbeddingQueryMetricType::COSINE));
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
+                                   /*semantic_score=*/0.1, document_id_0);
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
+                                   /*semantic_score=*/0.2, document_id_1);
+  ICING_ASSERT_OK_AND_ASSIGN(
+      score_map, embedding_query_results.GetOrCreateMatchInfoMap(
+                     /*query_vector_index=*/1,
+                     SearchSpecProto::EmbeddingQueryMetricType::COSINE));
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
+                                   /*semantic_score=*/-0.1, document_id_0);
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
+                                   /*semantic_score=*/-0.2, document_id_1);
+
+  // Create a weird scoring expression that has a non-constant query vector
+  // index. Specifically, document 0 should get the scores from the first query,
+  // but document 1 should get the scores from the second query.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<Scorer> scorer,
+      AdvancedScorer::Create(
+          CreateAdvancedScoringSpec(
+              "sum(this.matchedSemanticScores("
+              "getEmbeddingParameter(this.documentScore())))"),
+          kDefaultScore, /*default_semantic_metric_type=*/
+          SearchSpecProto::EmbeddingQueryMetricType::COSINE,
+          document_store_.get(), schema_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr, &embedding_query_results,
+          feature_flags_.get()));
+  EXPECT_THAT(scorer->GetScore(doc_hit_info_0), DoubleNear(0.1, kEps));
+  EXPECT_THAT(scorer->GetScore(doc_hit_info_1), DoubleNear(-0.2, kEps));
+}
+
 TEST_F(AdvancedScorerTest, ListRelatedFunctions) {
   DocumentId document_id_0 = 0;
   DocHitInfo doc_hit_info_0(document_id_0);
@@ -1717,23 +1831,31 @@
   //   {4, 5, 2, 1, 3}.
   // - this.matchedSemanticScores(getEmbeddingParameter(1)) returns an empty
   //   list.
-  EmbeddingQueryResults embedding_query_results;
-  EmbeddingQueryResults::EmbeddingQueryMatchInfoMap* score_map =
-      &embedding_query_results
-           .result_infos[0][SearchSpecProto::EmbeddingQueryMetricType::COSINE];
-  AddEntryToEmbeddingQueryScoreMap(*score_map,
+  EmbeddingQueryResults embedding_query_results(/*num_query_vectors=*/2);
+  ICING_ASSERT_OK_AND_ASSIGN(
+      EmbeddingQueryResults::EmbeddingQueryMatchInfoMap * score_map,
+      embedding_query_results.GetOrCreateMatchInfoMap(
+          /*query_vector_index=*/0,
+          SearchSpecProto::EmbeddingQueryMetricType::COSINE));
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
                                    /*semantic_score=*/4, document_id_0);
-  AddEntryToEmbeddingQueryScoreMap(*score_map,
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
                                    /*semantic_score=*/5, document_id_0);
-  AddEntryToEmbeddingQueryScoreMap(*score_map,
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
                                    /*semantic_score=*/2, document_id_0);
-  AddEntryToEmbeddingQueryScoreMap(*score_map,
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
                                    /*semantic_score=*/1, document_id_0);
-  AddEntryToEmbeddingQueryScoreMap(*score_map,
+  AddEntryToEmbeddingQueryScoreMap(*embedding_query_results.global_scores,
+                                   *score_map,
                                    /*semantic_score=*/3, document_id_0);
-  score_map =
-      &embedding_query_results
-           .result_infos[1][SearchSpecProto::EmbeddingQueryMetricType::COSINE];
+  ICING_ASSERT_OK_AND_ASSIGN(
+      score_map, embedding_query_results.GetOrCreateMatchInfoMap(
+                     /*query_vector_index=*/1,
+                     SearchSpecProto::EmbeddingQueryMetricType::COSINE));
 
   // maxOrDefault({4, 5, 2, 1, 3}, 100) = 5
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1810,8 +1932,8 @@
   const int64_t creation_timestamp_ms = 123;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(CreateDocument("namespace", "uri", /*score=*/123,
-                                          creation_timestamp_ms)));
+      document_store_->Put(document_util::CreateDocumentWrapper(CreateDocument(
+          "namespace", "uri", /*score=*/123, creation_timestamp_ms))));
   DocumentId document_id = put_result.new_document_id;
   DocHitInfo docHitInfo = DocHitInfo(document_id);
 
@@ -1941,8 +2063,9 @@
           .AddDoubleProperty("frequencyScore", 1.0, 2.0, 3.0)
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocHitInfo docHitInfo(put_result.new_document_id);
 
   ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
@@ -1977,8 +2100,9 @@
           .AddDoubleProperty("frequencyScore", 1.0, 2.0, 3.0)
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocHitInfo docHitInfo(put_result.new_document_id);
 
   ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
@@ -2013,8 +2137,9 @@
           .AddDoubleProperty("frequencyScore", 1.0, 2.0, 3.0)
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocHitInfo docHitInfo(put_result.new_document_id);
   ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
       "this.documentScore() + "
@@ -2047,8 +2172,9 @@
                                .SetScore(100)
                                .SetCreationTimestampMs(123)
                                .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocHitInfo docHitInfo(put_result.new_document_id);
 
   ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
@@ -2080,8 +2206,9 @@
                                .SetScore(100)
                                .SetCreationTimestampMs(123)
                                .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocHitInfo docHitInfo(put_result.new_document_id);
 
   // getScorableProperty("aliasPerson", "frequencyScore") will return an empty
@@ -2116,8 +2243,9 @@
                                .SetScore(100)
                                .SetCreationTimestampMs(123)
                                .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocHitInfo docHitInfo(put_result.new_document_id);
 
   // getScorableProperty("aliasPerson", "frequencyScore") will return an empty
@@ -2188,12 +2316,16 @@
   double expected_score_doc1 = 100 + (1 + 2 + 3);
   double expected_score_doc2 = 100 + 0;
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store_->Put(document_from_db1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document_from_db1)));
   DocHitInfo docHitInfo1(put_result1.new_document_id);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store_->Put(document_from_db2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store_->Put(
+          document_util::CreateDocumentWrapper(document_from_db2)));
   DocHitInfo docHitInfo2(put_result2.new_document_id);
 
   ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
@@ -2230,8 +2362,9 @@
           .AddDoubleProperty("frequencyScore", 1.0, 2.0, 3.0)
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocHitInfo docHitInfo(put_result.new_document_id);
 
   ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
@@ -2265,8 +2398,9 @@
                                .AddInt64Property("contactTimes", 10, 20, 30)
                                .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocHitInfo docHitInfo(put_result.new_document_id);
 
   ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
@@ -2301,8 +2435,9 @@
                                .AddInt64Property("contactTimes", 10, 20, 30)
                                .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocHitInfo docHitInfo(put_result.new_document_id);
 
   ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
@@ -2340,8 +2475,9 @@
           .AddDoubleProperty("frequencyScore", 1.0, 2.0, 3.0)
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocHitInfo docHitInfo(put_result.new_document_id);
 
   ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
@@ -2381,8 +2517,9 @@
           .AddDoubleProperty("frequencyScore", 1.0, 2.0, 3.0)
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocHitInfo docHitInfo(put_result.new_document_id);
 
   ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
@@ -2420,8 +2557,9 @@
                                .SetCreationTimestampMs(123)
                                .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocHitInfo docHitInfo(put_result.new_document_id);
 
   // Expected score will fall back to the default score, because max() throws an
@@ -2456,8 +2594,9 @@
                                .SetScore(100)
                                .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocHitInfo docHitInfo(put_result.new_document_id);
 
   ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
@@ -2589,8 +2728,9 @@
           .SetCreationTimestampMs(0)
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocHitInfo docHitInfo(put_result.new_document_id);
   ScoringSpecProto scoring_spec1 = CreateAdvancedScoringSpec(
       "this.documentScore() + "
@@ -2666,8 +2806,9 @@
           .AddDoubleProperty("frequencyScore", 1.0, 2.0, 3.0)
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store_->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(document_util::CreateDocumentWrapper(document)));
   DocHitInfo docHitInfo(put_result.new_document_id);
 
   ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
diff --git a/icing/scoring/advanced_scoring/double-list.h b/icing/scoring/advanced_scoring/double-list.h
new file mode 100644
index 0000000..6abc176
--- /dev/null
+++ b/icing/scoring/advanced_scoring/double-list.h
@@ -0,0 +1,99 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef ICING_SCORING_ADVANCED_SCORING_DOUBLE_LIST_H_
+#define ICING_SCORING_ADVANCED_SCORING_DOUBLE_LIST_H_
+
+#include <cstddef>
+#include <type_traits>
+#include <utility>
+#include <variant>
+#include <vector>
+
+// Represents the kDoubleList type, which can either own its data or provide a
+// non-owning view of existing data.
+class DoubleList {
+ public:
+  using value_type = double;
+
+  // Creates a list by taking ownership of an existing vector's data via move.
+  explicit DoubleList(std::vector<double>&& vec) : storage_(std::move(vec)) {}
+
+  // Creates a non-owning view of an external data buffer.
+  // The caller must ensure the lifetime of "data" exceeds the lifetime of this
+  // DoubleList.
+  explicit DoubleList(const double* data, size_t size)
+      : storage_(DataView(data, size)) {}
+
+  // Creates an empty list. Represents as an empty non-owning view by default.
+  explicit DoubleList() : storage_(DataView()) {}
+
+  // Disallow copy but allow move.
+  DoubleList(const DoubleList&) = delete;
+  DoubleList& operator=(const DoubleList&) = delete;
+  DoubleList(DoubleList&&) = default;
+  DoubleList& operator=(DoubleList&&) = default;
+
+  const double* data() const {
+    return std::visit(
+        [](const auto& arg) -> const double* { return arg.data(); }, storage_);
+  }
+
+  size_t size() const {
+    return std::visit([](const auto& arg) -> size_t { return arg.size(); },
+                      storage_);
+  }
+
+  const double* begin() const { return data(); }
+
+  const double* end() const { return data() + size(); }
+
+  bool empty() const { return size() == 0; }
+
+  // Releases the ownership of the internal vector, if owned.
+  // If not owned, returns a *new* vector containing a copy of the viewed data.
+  std::vector<double> ReleaseVector() && {
+    return std::visit(
+        [](auto&& arg) -> std::vector<double> {
+          using T = std::decay_t<decltype(arg)>;
+          if constexpr (std::is_same_v<T, std::vector<double>>) {
+            return std::forward<decltype(arg)>(arg);
+          } else {
+            return std::vector<double>(arg.data(), arg.data() + arg.size());
+          }
+        },
+        std::move(storage_));
+  }
+
+ private:
+  // Simple class to represent a non-owning view of data
+  class DataView {
+   public:
+    // Default constructor represents an empty non-owning view.
+    DataView() : DataView(nullptr, 0) {}
+    DataView(const double* data, size_t size) : data_(data), size_(size) {}
+
+    const double* data() const { return data_; }
+    size_t size() const { return size_; }
+
+   private:
+    const double* data_ = nullptr;
+    size_t size_ = 0;
+  };
+
+  // Storage can be either an owned vector or a non-owning view
+  std::variant<std::vector<double>, DataView> storage_;
+};
+
+#endif  // ICING_SCORING_ADVANCED_SCORING_DOUBLE_LIST_H_
diff --git a/icing/scoring/advanced_scoring/double-list_test.cc b/icing/scoring/advanced_scoring/double-list_test.cc
new file mode 100644
index 0000000..f29fe69
--- /dev/null
+++ b/icing/scoring/advanced_scoring/double-list_test.cc
@@ -0,0 +1,245 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/scoring/advanced_scoring/double-list.h"
+
+#include <cstddef>
+#include <iterator>
+#include <numeric>
+#include <utility>
+#include <vector>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace icing {
+namespace lib {
+
+namespace {
+
+using ::testing::ElementsAreArray;
+using ::testing::IsEmpty;
+
+std::vector<double> CreateSequenceVec(size_t size, double start = 0.0) {
+  std::vector<double> vec(size);
+  std::iota(vec.begin(), vec.end(), start);
+  return vec;
+}
+
+TEST(DoubleListTest, DefaultConstructorCreatesEmptyView) {
+  DoubleList list;
+  EXPECT_EQ(list.size(), 0);
+  EXPECT_TRUE(list.empty());
+  EXPECT_EQ(list.data(), nullptr);
+  EXPECT_EQ(list.begin(), list.end());
+}
+
+// Test constructor taking ownership of an empty vector
+TEST(DoubleListTest, EmptyVectorMoveConstructor) {
+  std::vector<double> source_vec;
+  const double* expected_data_ptr =
+      source_vec.data();  // Capture pointer before move
+  DoubleList list(std::move(source_vec));
+
+  EXPECT_EQ(list.size(), 0);
+  EXPECT_TRUE(list.empty());
+  EXPECT_EQ(list.data(), expected_data_ptr);
+  EXPECT_EQ(list.begin(), list.end());
+}
+
+// Test constructor creating an empty non-owning view, which is the same
+// behavior of the default constructor.
+TEST(DoubleListTest, EmptyDataViewConstructor) {
+  const double* external_data = nullptr;
+  DoubleList list(external_data, 0);
+  EXPECT_EQ(list.size(), 0);
+  EXPECT_TRUE(list.empty());
+  EXPECT_EQ(list.data(), nullptr);
+  EXPECT_EQ(list.begin(), list.end());
+}
+
+// Test constructor taking ownership via std::vector rvalue reference
+TEST(DoubleListTest, VectorMoveConstructor) {
+  std::vector<double> source_vec = {1.1, 2.2, 3.3};
+  const double* expected_data_ptr =
+      source_vec.data();  // Capture pointer before move
+  const size_t expected_size = source_vec.size();
+
+  DoubleList list(std::move(source_vec));
+
+  EXPECT_EQ(list.size(), expected_size);
+  ASSERT_FALSE(list.empty());
+  EXPECT_EQ(list.data(), expected_data_ptr);
+  EXPECT_THAT(list, ElementsAreArray({1.1, 2.2, 3.3}));
+}
+
+// Test constructor creating a non-owning view
+TEST(DoubleListTest, DataViewConstructor) {
+  const double external_data[] = {4.4, 5.5, 6.6};
+  const size_t external_size = std::size(external_data);
+
+  DoubleList list(external_data, external_size);
+
+  EXPECT_EQ(list.size(), external_size);
+  ASSERT_FALSE(list.empty());
+  EXPECT_EQ(list.data(),
+            external_data);  // Should point directly to external data
+  EXPECT_THAT(list, ElementsAreArray({4.4, 5.5, 6.6}));
+}
+
+// Test accessors on an owned list
+TEST(DoubleListTest, AccessorsOwned) {
+  const DoubleList list(CreateSequenceVec(3, 10.0));  // {10.0, 11.0, 12.0}
+
+  EXPECT_EQ(list.size(), 3);
+  ASSERT_FALSE(list.empty());
+  ASSERT_NE(list.data(), nullptr);
+  EXPECT_EQ(list.begin()[0], 10.0);
+  EXPECT_EQ(list.begin()[1], 11.0);
+  EXPECT_EQ(list.begin()[2], 12.0);
+  EXPECT_EQ(list.end(), list.begin() + 3);
+}
+
+// Test accessors on a non-owning list
+TEST(DoubleListTest, AccessorsView) {
+  const double external_data[] = {20.0, 21.0};
+  const DoubleList list(external_data, 2);
+
+  EXPECT_EQ(list.size(), 2);
+  ASSERT_FALSE(list.empty());
+  ASSERT_EQ(list.data(), external_data);
+  EXPECT_EQ(list.begin()[0], 20.0);
+  EXPECT_EQ(list.begin()[1], 21.0);
+  EXPECT_EQ(list.end(), list.begin() + 2);
+}
+
+// Test move constructor from an owned list
+TEST(DoubleListTest, MoveConstructionFromOwned) {
+  DoubleList list1(CreateSequenceVec(2, 1.0));  // {1.0, 2.0}
+  const double* original_data_ptr = list1.data();
+
+  DoubleList list2(std::move(list1));
+
+  // Check list2 has the data
+  EXPECT_EQ(list2.size(), 2);
+  ASSERT_FALSE(list2.empty());
+  EXPECT_EQ(list2.data(), original_data_ptr);
+  EXPECT_THAT(list2, ElementsAreArray({1.0, 2.0}));
+}
+
+// Test move constructor from a non-owning list
+TEST(DoubleListTest, MoveConstructionFromView) {
+  const double external_data[] = {3.0, 4.0};
+  DoubleList list1(external_data, 2);
+  const size_t original_size = list1.size();
+
+  DoubleList list2(std::move(list1));
+
+  // Check list2 has the view details
+  EXPECT_EQ(list2.size(), original_size);
+  ASSERT_FALSE(list2.empty());
+  EXPECT_EQ(list2.data(), external_data);
+  EXPECT_THAT(list2, ElementsAreArray({3.0, 4.0}));
+}
+
+// Test move assignment from an owned list to another
+TEST(DoubleListTest, MoveAssignmentFromOwned) {
+  DoubleList list1(CreateSequenceVec(3, 5.0));    // {5.0, 6.0, 7.0}
+  DoubleList list2(CreateSequenceVec(1, 100.0));  // {100.0}
+  const double* original_data_ptr_list1 = list1.data();
+
+  list2 = std::move(list1);
+
+  // Check list2 has the data from list1
+  EXPECT_EQ(list2.size(), 3);
+  ASSERT_FALSE(list2.empty());
+  EXPECT_EQ(list2.data(), original_data_ptr_list1);
+  EXPECT_THAT(list2, ElementsAreArray({5.0, 6.0, 7.0}));
+}
+
+// Test move assignment from a non-owning list to another
+TEST(DoubleListTest, MoveAssignmentFromView) {
+  const double external_data[] = {8.0, 9.0};
+  DoubleList list1(external_data, 2);
+  DoubleList list2(CreateSequenceVec(1, 200.0));  // {200.0}
+
+  list2 = std::move(list1);
+
+  // Check list2 has the view details from list1
+  EXPECT_EQ(list2.size(), 2);
+  ASSERT_FALSE(list2.empty());
+  EXPECT_EQ(list2.data(), external_data);
+  EXPECT_THAT(list2, ElementsAreArray({8.0, 9.0}));
+}
+
+// Test ReleaseVector when the list owns the data
+TEST(DoubleListTest, ReleaseVectorOwned) {
+  DoubleList list(CreateSequenceVec(3, 10.0));  // {10.0, 11.0, 12.0}
+  const double* original_data_ptr = list.data();
+
+  // ReleaseVector must be called on an rvalue
+  std::vector<double> released_vec = std::move(list).ReleaseVector();
+
+  // Check the released vector
+  EXPECT_EQ(released_vec.size(), 3);
+  EXPECT_THAT(released_vec, ElementsAreArray({10.0, 11.0, 12.0}));
+  EXPECT_EQ(released_vec.data(), original_data_ptr);
+}
+
+// Test ReleaseVector when the list has a non-owning view
+TEST(DoubleListTest, ReleaseVectorView) {
+  const double external_data[] = {13.0, 14.0};
+  DoubleList list(external_data, 2);
+  const double* original_view_ptr = list.data();
+
+  // ReleaseVector must be called on an rvalue
+  std::vector<double> released_vec = std::move(list).ReleaseVector();
+
+  // Check the released vector - it should be a COPY
+  EXPECT_EQ(released_vec.size(), 2);
+  EXPECT_THAT(released_vec, ElementsAreArray({13.0, 14.0}));
+
+  // IMPORTANT: Verify it's a copy, not pointing to the original external data
+  EXPECT_NE(released_vec.data(), original_view_ptr);
+}
+
+// Test ReleaseVector when the list is empty (default constructed - view)
+TEST(DoubleListTest, ReleaseVectorEmptyDefault) {
+  DoubleList list;  // Empty view by default
+  std::vector<double> released_vec = std::move(list).ReleaseVector();
+  EXPECT_TRUE(released_vec.empty());
+  EXPECT_THAT(released_vec, IsEmpty());
+}
+
+// Test ReleaseVector when the list is empty (constructed from empty view)
+TEST(DoubleListTest, ReleaseVectorEmptyView) {
+  DoubleList list(nullptr, 0);  // Empty view
+  std::vector<double> released_vec = std::move(list).ReleaseVector();
+  EXPECT_TRUE(released_vec.empty());
+  EXPECT_THAT(released_vec, IsEmpty());
+}
+
+// Test ReleaseVector when the list is empty (constructed from empty vector -
+// owned)
+TEST(DoubleListTest, ReleaseVectorEmptyOwned) {
+  DoubleList list(std::vector<double>{});  // Empty owned vector
+  std::vector<double> released_vec = std::move(list).ReleaseVector();
+  EXPECT_TRUE(released_vec.empty());
+  EXPECT_THAT(released_vec, IsEmpty());
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/scoring/advanced_scoring/score-expression-util_test.cc b/icing/scoring/advanced_scoring/score-expression-util_test.cc
index 4e4355b..51b849a 100644
--- a/icing/scoring/advanced_scoring/score-expression-util_test.cc
+++ b/icing/scoring/advanced_scoring/score-expression-util_test.cc
@@ -29,6 +29,7 @@
 #include "icing/file/filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/index/hit/doc-hit-info.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/proto/scoring.pb.h"
@@ -42,6 +43,7 @@
 #include "icing/testing/fake-clock.h"
 #include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/document-util.h"
 
 namespace icing {
 namespace lib {
@@ -72,14 +74,18 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, doc_store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, doc_store_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
 
     // Creates the schema
@@ -204,8 +210,8 @@
 TEST_F(ScoreExpressionUtilTest, SimpleExpression) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(CreateDocument("namespace", "uri", /*score=*/10,
-                                          /*creation_timestamp_ms=*/100)));
+      document_store_->Put(document_util::CreateDocumentWrapper(CreateDocument(
+          "namespace", "uri", /*score=*/10, /*creation_timestamp_ms=*/100))));
   DocumentId document_id = put_result.new_document_id;
   DocHitInfo doc_hit_info = DocHitInfo(document_id);
 
@@ -228,8 +234,8 @@
 TEST_F(ScoreExpressionUtilTest, DocumentFunctionsWithoutOptionalDependencies) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      document_store_->Put(CreateDocument("namespace", "uri", /*score=*/10,
-                                          /*creation_timestamp_ms=*/100)));
+      document_store_->Put(document_util::CreateDocumentWrapper(CreateDocument(
+          "namespace", "uri", /*score=*/10, /*creation_timestamp_ms=*/100))));
   DocumentId document_id = put_result.new_document_id;
   DocHitInfo doc_hit_info = DocHitInfo(document_id);
 
diff --git a/icing/scoring/advanced_scoring/score-expression.cc b/icing/scoring/advanced_scoring/score-expression.cc
index 445e1f3..2dcd20d 100644
--- a/icing/scoring/advanced_scoring/score-expression.cc
+++ b/icing/scoring/advanced_scoring/score-expression.cc
@@ -41,6 +41,7 @@
 #include "icing/proto/internal/scorable_property_set.pb.h"
 #include "icing/schema/schema-store.h"
 #include "icing/schema/section.h"
+#include "icing/scoring/advanced_scoring/double-list.h"
 #include "icing/scoring/bm25f-calculator.h"
 #include "icing/scoring/scored-document-hit.h"
 #include "icing/scoring/section-weights.h"
@@ -312,89 +313,106 @@
 libtextclassifier3::StatusOr<double>
 MathFunctionScoreExpression::EvaluateDouble(
     const DocHitInfo& hit_info, const DocHitInfoIterator* query_it) const {
-  std::vector<double> values;
+  DoubleList list_value;
+  std::vector<double> double_values;
   int ind = 0;
   if (args_.at(0)->type() == ScoreExpressionType::kDoubleList) {
-    ICING_ASSIGN_OR_RETURN(values,
+    ICING_ASSIGN_OR_RETURN(list_value,
                            args_.at(0)->EvaluateList(hit_info, query_it));
     ind = 1;
   }
+  double_values.reserve(args_.size() - ind);
   for (; ind < args_.size(); ++ind) {
     ICING_ASSIGN_OR_RETURN(double v,
                            args_.at(ind)->EvaluateDouble(hit_info, query_it));
-    values.push_back(v);
+    double_values.push_back(v);
+  }
+
+  // Double values in variable-argument functions can be treated as a single
+  // list.
+  if (kVariableArgumentsFunctions.count(function_type_) > 0 &&
+      !double_values.empty()) {
+    if (!list_value.empty()) {
+      return absl_ports::InternalError(
+          "Should never reach here, since static type checking should not "
+          "allow variable-argument functions to have both list arguments and "
+          "double arguments.");
+    }
+    list_value = DoubleList(std::move(double_values));
+    double_values.clear();
   }
 
   double res = 0;
   switch (function_type_) {
     case FunctionType::kLog:
-      if (values.size() == 1) {
-        res = log(values[0]);
+      if (double_values.size() == 1) {
+        res = log(double_values[0]);
       } else {
         // argument 0 is log base
         // argument 1 is the value
-        res = log(values[1]) / log(values[0]);
+        res = log(double_values[1]) / log(double_values[0]);
       }
       break;
     case FunctionType::kPow:
-      res = pow(values[0], values[1]);
+      res = pow(double_values[0], double_values[1]);
       break;
+    case FunctionType::kSqrt:
+      res = sqrt(double_values[0]);
+      break;
+    case FunctionType::kAbs:
+      res = abs(double_values[0]);
+      break;
+    case FunctionType::kSin:
+      res = sin(double_values[0]);
+      break;
+    case FunctionType::kCos:
+      res = cos(double_values[0]);
+      break;
+    case FunctionType::kTan:
+      res = tan(double_values[0]);
+      break;
+    // Variable-argument functions
     case FunctionType::kMax:
-      if (values.empty()) {
+      if (list_value.empty()) {
         return absl_ports::InvalidArgumentError(
             "Got an empty parameter set in max function");
       }
-      res = *std::max_element(values.begin(), values.end());
+      res = *std::max_element(list_value.begin(), list_value.end());
       break;
     case FunctionType::kMin:
-      if (values.empty()) {
+      if (list_value.empty()) {
         return absl_ports::InvalidArgumentError(
             "Got an empty parameter set in min function");
       }
-      res = *std::min_element(values.begin(), values.end());
+      res = *std::min_element(list_value.begin(), list_value.end());
       break;
     case FunctionType::kLen:
-      res = values.size();
+      res = list_value.size();
       break;
     case FunctionType::kSum:
-      res = std::reduce(values.begin(), values.end());
+      res = std::reduce(list_value.begin(), list_value.end());
       break;
     case FunctionType::kAvg:
-      if (values.empty()) {
+      if (list_value.empty()) {
         return absl_ports::InvalidArgumentError(
             "Got an empty parameter set in avg function.");
       }
-      res = std::reduce(values.begin(), values.end()) / values.size();
+      res =
+          std::reduce(list_value.begin(), list_value.end()) / list_value.size();
       break;
-    case FunctionType::kSqrt:
-      res = sqrt(values[0]);
-      break;
-    case FunctionType::kAbs:
-      res = abs(values[0]);
-      break;
-    case FunctionType::kSin:
-      res = sin(values[0]);
-      break;
-    case FunctionType::kCos:
-      res = cos(values[0]);
-      break;
-    case FunctionType::kTan:
-      res = tan(values[0]);
-      break;
-    // For the following two functions, the last value is the default value.
-    // If values.size() == 1, then it means the provided list is empty.
+    // For the following two functions, double_values[0] is the default value.
     case FunctionType::kMaxOrDefault:
-      if (values.size() == 1) {
-        res = values[0];
+      if (list_value.empty()) {
+        res = double_values[0];
       } else {
-        res = *std::max_element(values.begin(), values.end() - 1);
+        res = *std::max_element(list_value.begin(), list_value.end());
       }
       break;
     case FunctionType::kMinOrDefault:
-      if (values.size() == 1) {
-        res = values[0];
+      if (list_value.empty()) {
+        res = double_values[0];
       } else {
-        res = *std::min_element(values.begin(), values.end() - 1);
+        res = *std::min_element(list_value.begin(), list_value.end());
       }
       break;
   }
@@ -444,12 +462,12 @@
       new ListOperationFunctionScoreExpression(function_type, std::move(args)));
 }
 
-libtextclassifier3::StatusOr<std::vector<double>>
+libtextclassifier3::StatusOr<DoubleList>
 ListOperationFunctionScoreExpression::EvaluateList(
     const DocHitInfo& hit_info, const DocHitInfoIterator* query_it) const {
   switch (function_type_) {
     case FunctionType::kFilterByRange:
-      ICING_ASSIGN_OR_RETURN(std::vector<double> list_value,
+      ICING_ASSIGN_OR_RETURN(DoubleList list_value,
                              args_.at(0)->EvaluateList(hit_info, query_it));
       ICING_ASSIGN_OR_RETURN(double low,
                              args_.at(1)->EvaluateDouble(hit_info, query_it));
@@ -459,11 +477,13 @@
         return absl_ports::InvalidArgumentError(
             "The lower bound cannot be greater than the upper bound.");
       }
+      // TODO(b/408437387): Consider avoiding a copy if nothing is filtered out.
+      std::vector<double> new_list = std::move(list_value).ReleaseVector();
       auto new_end =
-          std::remove_if(list_value.begin(), list_value.end(),
+          std::remove_if(new_list.begin(), new_list.end(),
                          [low, high](double v) { return v < low || v > high; });
-      list_value.erase(new_end, list_value.end());
-      return list_value;
+      new_list.erase(new_end, new_list.end());
+      return DoubleList(std::move(new_list));
       break;
   }
   return absl_ports::InternalError("Should never reach here.");
@@ -629,7 +649,7 @@
           document_store, *join_children_fetcher, current_time_ms));
 }
 
-libtextclassifier3::StatusOr<std::vector<double>>
+libtextclassifier3::StatusOr<DoubleList>
 ChildrenRankingSignalsFunctionScoreExpression::EvaluateList(
     const DocHitInfo& hit_info, const DocHitInfoIterator* query_it) const {
   ICING_ASSIGN_OR_RETURN(
@@ -640,7 +660,7 @@
   for (const ScoredDocumentHit& child_hit : children_hits) {
     children_scores.push_back(child_hit.score());
   }
-  return std::move(children_scores);
+  return DoubleList(std::move(children_scores));
 }
 
 libtextclassifier3::StatusOr<
@@ -664,7 +684,7 @@
           document_store, section_weights, current_time_ms));
 }
 
-libtextclassifier3::StatusOr<std::vector<double>>
+libtextclassifier3::StatusOr<DoubleList>
 PropertyWeightsFunctionScoreExpression::EvaluateList(
     const DocHitInfo& hit_info, const DocHitInfoIterator*) const {
   std::vector<double> weights;
@@ -678,7 +698,7 @@
     weights.push_back(section_weights_.GetNormalizedSectionWeight(
         schema_type_id, section_id));
   }
-  return weights;
+  return DoubleList(std::move(weights));
 }
 
 libtextclassifier3::StatusOr<std::unique_ptr<ScoreExpression>>
@@ -767,13 +787,16 @@
         metric_type,
         embedding_util::GetEmbeddingQueryMetricTypeFromName(metric));
   }
+  const EmbeddingQueryResults::EmbeddingQueryMatchInfoMap* match_info_map_ =
+      nullptr;
   if (embedding_index_arg->is_constant()) {
     ICING_ASSIGN_OR_RETURN(
         uint32_t embedding_index,
         embedding_index_arg->EvaluateDouble(DocHitInfo(),
                                             /*query_it=*/nullptr));
-    if (embedding_query_results->GetMatchInfoMap(embedding_index,
-                                                 metric_type) == nullptr) {
+    match_info_map_ =
+        embedding_query_results->GetMatchInfoMap(embedding_index, metric_type);
+    if (match_info_map_ == nullptr) {
       return absl_ports::InvalidArgumentError(absl_ports::StrCat(
           "The embedding query index ", std::to_string(embedding_index),
           " with metric type ",
@@ -783,22 +806,26 @@
   }
   return std::unique_ptr<MatchedSemanticScoresFunctionScoreExpression>(
       new MatchedSemanticScoresFunctionScoreExpression(
-          std::move(args), metric_type, *embedding_query_results));
+          std::move(args), metric_type, *embedding_query_results,
+          match_info_map_));
 }
 
-libtextclassifier3::StatusOr<std::vector<double>>
+libtextclassifier3::StatusOr<DoubleList>
 MatchedSemanticScoresFunctionScoreExpression::EvaluateList(
     const DocHitInfo& hit_info, const DocHitInfoIterator* query_it) const {
   ICING_ASSIGN_OR_RETURN(double raw_query_index,
                          args_[1]->EvaluateDouble(hit_info, query_it));
   uint32_t query_index = (uint32_t)raw_query_index;
-  const std::vector<double>* scores =
-      embedding_query_results_.GetMatchedScoresForDocument(
-          query_index, metric_type_, hit_info.document_id());
-  if (scores == nullptr) {
-    return std::vector<double>();
+  if (match_info_map_ != nullptr) {
+    auto info_it = match_info_map_->find(hit_info.document_id());
+    if (info_it == match_info_map_->end()) {
+      return DoubleList();
+    }
+    return embedding_query_results_.GetMatchedScoresFromEmbeddingMatchInfos(
+        info_it->second);
   }
-  return *scores;
+  return embedding_query_results_.GetMatchedScoresForDocument(
+      query_index, metric_type_, hit_info.document_id());
 }
 
 GetScorablePropertyFunctionScoreExpression::
@@ -885,13 +912,13 @@
           std::move(schema_type_ids), property_path));
 }
 
-libtextclassifier3::StatusOr<std::vector<double>>
+libtextclassifier3::StatusOr<DoubleList>
 GetScorablePropertyFunctionScoreExpression::EvaluateList(
     const DocHitInfo& hit_info, const DocHitInfoIterator* query_it) const {
   SchemaTypeId doc_schema_type_id = GetSchemaTypeId(
       hit_info.document_id(), document_store_, current_time_ms_);
   if (schema_type_ids_.find(doc_schema_type_id) == schema_type_ids_.end()) {
-    return std::vector<double>();
+    return DoubleList();
   }
 
   // By this point, the document to be evaluated is guaranteed to have a
@@ -920,17 +947,19 @@
 
   // Converts ScorablePropertyProto to a vector of doubles.
   if (scorable_property_proto->int64_values_size() > 0) {
-    return std::vector<double>(scorable_property_proto->int64_values().begin(),
-                               scorable_property_proto->int64_values().end());
+    return DoubleList(
+        std::vector<double>(scorable_property_proto->int64_values().begin(),
+                            scorable_property_proto->int64_values().end()));
   } else if (scorable_property_proto->double_values_size() > 0) {
-    return std::vector<double>(scorable_property_proto->double_values().begin(),
-                               scorable_property_proto->double_values().end());
+    return DoubleList(
+        std::vector<double>(scorable_property_proto->double_values().begin(),
+                            scorable_property_proto->double_values().end()));
   } else if (scorable_property_proto->boolean_values_size() > 0) {
-    return std::vector<double>(
-        scorable_property_proto->boolean_values().begin(),
-        scorable_property_proto->boolean_values().end());
+    return DoubleList(
+        std::vector<double>(scorable_property_proto->boolean_values().begin(),
+                            scorable_property_proto->boolean_values().end()));
   }
-  return std::vector<double>();
+  return DoubleList();
 }
 
 }  // namespace lib
diff --git a/icing/scoring/advanced_scoring/score-expression.h b/icing/scoring/advanced_scoring/score-expression.h
index 53c5807..5df15ec 100644
--- a/icing/scoring/advanced_scoring/score-expression.h
+++ b/icing/scoring/advanced_scoring/score-expression.h
@@ -31,12 +31,11 @@
 #include "icing/index/iterator/doc-hit-info-iterator.h"
 #include "icing/join/join-children-fetcher.h"
 #include "icing/schema/schema-store.h"
+#include "icing/scoring/advanced_scoring/double-list.h"
 #include "icing/scoring/bm25f-calculator.h"
 #include "icing/scoring/section-weights.h"
 #include "icing/store/document-filter-data.h"
-#include "icing/store/document-id.h"
 #include "icing/store/document-store.h"
-#include "icing/util/status-macros.h"
 
 namespace icing {
 namespace lib {
@@ -78,7 +77,7 @@
         "double. There must be inconsistencies in the static type checking.");
   }
 
-  virtual libtextclassifier3::StatusOr<std::vector<double>> EvaluateList(
+  virtual libtextclassifier3::StatusOr<DoubleList> EvaluateList(
       const DocHitInfo& hit_info, const DocHitInfoIterator* query_it) const {
     if (type() == ScoreExpressionType::kDoubleList) {
       return absl_ports::UnimplementedError(
@@ -269,7 +268,7 @@
       FunctionType function_type,
       std::vector<std::unique_ptr<ScoreExpression>> args);
 
-  libtextclassifier3::StatusOr<std::vector<double>> EvaluateList(
+  libtextclassifier3::StatusOr<DoubleList> EvaluateList(
       const DocHitInfo& hit_info,
       const DocHitInfoIterator* query_it) const override;
 
@@ -381,7 +380,7 @@
          const JoinChildrenFetcher* join_children_fetcher,
          int64_t current_time_ms);
 
-  libtextclassifier3::StatusOr<std::vector<double>> EvaluateList(
+  libtextclassifier3::StatusOr<DoubleList> EvaluateList(
       const DocHitInfo& hit_info,
       const DocHitInfoIterator* query_it) const override;
 
@@ -416,7 +415,7 @@
          const DocumentStore* document_store,
          const SectionWeights* section_weights, int64_t current_time_ms);
 
-  libtextclassifier3::StatusOr<std::vector<double>> EvaluateList(
+  libtextclassifier3::StatusOr<DoubleList> EvaluateList(
       const DocHitInfo& hit_info, const DocHitInfoIterator*) const override;
 
   ScoreExpressionType type() const override {
@@ -477,7 +476,7 @@
          SearchSpecProto::EmbeddingQueryMetricType::Code default_metric_type,
          const EmbeddingQueryResults* embedding_query_results);
 
-  libtextclassifier3::StatusOr<std::vector<double>> EvaluateList(
+  libtextclassifier3::StatusOr<DoubleList> EvaluateList(
       const DocHitInfo& hit_info,
       const DocHitInfoIterator* query_it) const override;
 
@@ -489,14 +488,20 @@
   explicit MatchedSemanticScoresFunctionScoreExpression(
       std::vector<std::unique_ptr<ScoreExpression>> args,
       SearchSpecProto::EmbeddingQueryMetricType::Code metric_type,
-      const EmbeddingQueryResults& embedding_query_results)
+      const EmbeddingQueryResults& embedding_query_results,
+      const EmbeddingQueryResults::EmbeddingQueryMatchInfoMap* match_info_map)
       : args_(std::move(args)),
         metric_type_(metric_type),
-        embedding_query_results_(embedding_query_results) {}
+        embedding_query_results_(embedding_query_results),
+        match_info_map_(match_info_map) {}
 
   std::vector<std::unique_ptr<ScoreExpression>> args_;
   const SearchSpecProto::EmbeddingQueryMetricType::Code metric_type_;
   const EmbeddingQueryResults& embedding_query_results_;
+  // If the embedding vector's index evaluated from args is a constant, this is
+  // the corresponding EmbeddingQueryMatchInfoMap for the embedding query.
+  // Otherwise, this is nullptr.
+  const EmbeddingQueryResults::EmbeddingQueryMatchInfoMap* match_info_map_;
 };
 
 class GetScorablePropertyFunctionScoreExpression : public ScoreExpression {
@@ -522,7 +527,7 @@
     return ScoreExpressionType::kDoubleList;
   }
 
-  libtextclassifier3::StatusOr<std::vector<double>> EvaluateList(
+  libtextclassifier3::StatusOr<DoubleList> EvaluateList(
       const DocHitInfo& hit_info,
       const DocHitInfoIterator* query_it) const override;
 
diff --git a/icing/scoring/advanced_scoring/score-expression_test.cc b/icing/scoring/advanced_scoring/score-expression_test.cc
index cbfed9b..391248d 100644
--- a/icing/scoring/advanced_scoring/score-expression_test.cc
+++ b/icing/scoring/advanced_scoring/score-expression_test.cc
@@ -24,6 +24,7 @@
 #include "gtest/gtest.h"
 #include "icing/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/scoring/advanced_scoring/double-list.h"
 #include "icing/testing/common-matchers.h"
 
 namespace icing {
@@ -62,9 +63,9 @@
     return res;
   }
 
-  libtextclassifier3::StatusOr<std::vector<double>> EvaluateList(
+  libtextclassifier3::StatusOr<DoubleList> EvaluateList(
       const DocHitInfo &, const DocHitInfoIterator *) const override {
-    return values;
+    return DoubleList(values.data(), values.size());
   }
 
   ScoreExpressionType type() const override {
diff --git a/icing/scoring/priority-queue-scored-document-hits-ranker.h b/icing/scoring/priority-queue-scored-document-hits-ranker.h
index 68cf921..135e0e3 100644
--- a/icing/scoring/priority-queue-scored-document-hits-ranker.h
+++ b/icing/scoring/priority-queue-scored-document-hits-ranker.h
@@ -15,6 +15,7 @@
 #ifndef ICING_SCORING_PRIORITY_QUEUE_SCORED_DOCUMENT_HITS_RANKER_H_
 #define ICING_SCORING_PRIORITY_QUEUE_SCORED_DOCUMENT_HITS_RANKER_H_
 
+#include <memory>
 #include <queue>
 #include <unordered_set>
 #include <vector>
@@ -37,22 +38,23 @@
 
   ~PriorityQueueScoredDocumentHitsRanker() override = default;
 
+  void Pop() override;
+
   // Note: ranker may store ScoredDocumentHit or JoinedScoredDocumentHit, so we
   // have template for scored_data_pq_.
   // - JoinedScoredDocumentHit is a superset of ScoredDocumentHit, so we unify
-  //   the return type of PopNext to use the superset type
-  //   JoinedScoredDocumentHit in order to make it simple, and rankers storing
-  //   ScoredDocumentHit should convert it to JoinedScoredDocumentHit before
-  //   returning. It makes the implementation simpler, especially for
-  //   ResultRetriever, which now only needs to deal with one single return
-  //   format.
+  //   the return type of Top to use the superset type JoinedScoredDocumentHit
+  //   in order to make it simple, and rankers storing ScoredDocumentHit should
+  //   convert it to JoinedScoredDocumentHit before returning. It makes the
+  //   implementation simpler, especially for ResultRetriever, which now only
+  //   needs to deal with one single return format.
   // - JoinedScoredDocumentHit has ~2x size of ScoredDocumentHit. Since we cache
   //   ranker (which contains a priority queue of data) in ResultState, if we
   //   store the scored hits in JoinedScoredDocumentHit format directly, then it
   //   doubles the memory usage. Therefore, we still keep the flexibility to
-  //   store ScoredDocumentHit or any other types of data, but require PopNext
-  //   to convert it to JoinedScoredDocumentHit.
-  JoinedScoredDocumentHit PopNext() override;
+  //   store ScoredDocumentHit or any other types of data, but require Pop to
+  //   convert it to JoinedScoredDocumentHit and cache it in curr_.
+  const JoinedScoredDocumentHit& Top() const override { return *curr_; }
 
   // Returns DocumentIds of the top K documents according to the ranking policy.
   // - For ScoredDocumentHit, this returns the DocumentIds of the top K
@@ -70,7 +72,7 @@
 
   int size() const override { return scored_data_pq_.size(); }
 
-  bool empty() const override { return scored_data_pq_.empty(); }
+  bool empty() const override { return curr_ == nullptr; }
 
  private:
   // Comparator for std::priority_queue. Since std::priority is a max heap
@@ -95,6 +97,11 @@
     bool is_ascending_;
   };
 
+  // Helper function to refresh the current element (fetch the top element from
+  // the priority queue, convert it to JoinedScoredDocumentHit, and cache it in
+  // curr_).
+  void RefreshCurrent();
+
   Comparator comparator_;
 
   // Use priority queue to get top K hits in O(KlgN) time.
@@ -102,6 +109,8 @@
       scored_data_pq_;
 
   Converter converter_;
+
+  std::unique_ptr<JoinedScoredDocumentHit> curr_;
 };
 
 template <typename ScoredDataType, typename Converter>
@@ -109,14 +118,14 @@
     PriorityQueueScoredDocumentHitsRanker(
         std::vector<ScoredDataType>&& scored_data_vec, bool is_descending)
     : comparator_(/*is_ascending=*/!is_descending),
-      scored_data_pq_(comparator_, std::move(scored_data_vec)) {}
+      scored_data_pq_(comparator_, std::move(scored_data_vec)) {
+  RefreshCurrent();
+}
 
 template <typename ScoredDataType, typename Converter>
-JoinedScoredDocumentHit
-PriorityQueueScoredDocumentHitsRanker<ScoredDataType, Converter>::PopNext() {
-  ScoredDataType next_scored_data = scored_data_pq_.top();
+void PriorityQueueScoredDocumentHitsRanker<ScoredDataType, Converter>::Pop() {
   scored_data_pq_.pop();
-  return converter_(std::move(next_scored_data));
+  RefreshCurrent();
 }
 
 template <typename ScoredDataType, typename Converter>
@@ -192,7 +201,27 @@
     new_pq.push(scored_data_pq_.top());
     scored_data_pq_.pop();
   }
+
+  // Assign back to the class members.
   scored_data_pq_ = std::move(new_pq);
+  RefreshCurrent();
+}
+
+template <typename ScoredDataType, typename Converter>
+void PriorityQueueScoredDocumentHitsRanker<ScoredDataType,
+                                           Converter>::RefreshCurrent() {
+  if (scored_data_pq_.empty()) {
+    curr_ = nullptr;
+  } else {
+    ScoredDataType scored_data = scored_data_pq_.top();
+
+    if (curr_ == nullptr) {
+      curr_ = std::make_unique<JoinedScoredDocumentHit>(
+          converter_(std::move(scored_data)));
+    } else {
+      *curr_ = converter_(std::move(scored_data));
+    }
+  }
 }
 
 }  // namespace lib
diff --git a/icing/scoring/priority-queue-scored-document-hits-ranker_test.cc b/icing/scoring/priority-queue-scored-document-hits-ranker_test.cc
index 19a9ebb..7ec1d68 100644
--- a/icing/scoring/priority-queue-scored-document-hits-ranker_test.cc
+++ b/icing/scoring/priority-queue-scored-document-hits-ranker_test.cc
@@ -48,7 +48,8 @@
     PriorityQueueScoredDocumentHitsRanker<ScoredDocumentHit>& ranker) {
   std::vector<JoinedScoredDocumentHit> hits;
   while (!ranker.empty()) {
-    hits.push_back(ranker.PopNext());
+    hits.push_back(ranker.Top());
+    ranker.Pop();
   }
   return hits;
 }
@@ -57,7 +58,8 @@
     PriorityQueueScoredDocumentHitsRanker<JoinedScoredDocumentHit>& ranker) {
   std::vector<JoinedScoredDocumentHit> hits;
   while (!ranker.empty()) {
-    hits.push_back(ranker.PopNext());
+    hits.push_back(ranker.Top());
+    ranker.Pop();
   }
   return hits;
 }
@@ -76,15 +78,15 @@
   EXPECT_THAT(ranker.size(), Eq(3));
   EXPECT_FALSE(ranker.empty());
 
-  ranker.PopNext();
+  ranker.Pop();
   EXPECT_THAT(ranker.size(), Eq(2));
   EXPECT_FALSE(ranker.empty());
 
-  ranker.PopNext();
+  ranker.Pop();
   EXPECT_THAT(ranker.size(), Eq(1));
   EXPECT_FALSE(ranker.empty());
 
-  ranker.PopNext();
+  ranker.Pop();
   EXPECT_THAT(ranker.size(), Eq(0));
   EXPECT_TRUE(ranker.empty());
 }
diff --git a/icing/scoring/score-and-rank_benchmark.cc b/icing/scoring/score-and-rank_benchmark.cc
index c8f6c00..006457f 100644
--- a/icing/scoring/score-and-rank_benchmark.cc
+++ b/icing/scoring/score-and-rank_benchmark.cc
@@ -32,6 +32,7 @@
 #include "icing/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator-test-util.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/proto/scoring.pb.h"
@@ -46,6 +47,7 @@
 #include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 #include "icing/util/clock.h"
+#include "icing/util/document-util.h"
 
 // This is an overall benchmark for ScoringProcessor, Scorer, and Ranker. It
 // shows how performance varies when we score different numbers of document
@@ -85,6 +87,12 @@
   return schema;
 }
 
+DocumentWrapper CreateDocumentWrapper(DocumentProto document,
+                                      int32_t num_string_tokens) {
+  document.mutable_internal_fields()->set_length_in_tokens(num_string_tokens);
+  return document_util::CreateDocumentWrapper(std::move(document));
+}
+
 DocumentProto CreateEmailDocument(int id, int document_score,
                                   uint64_t creation_timestamp_ms) {
   return DocumentBuilder()
@@ -105,6 +113,9 @@
       /*force_recovery_and_revalidate_documents=*/false,
       /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
       PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
+      PortableFileBackedProtoLog<
+          DocumentWrapper>::kDefaultCompressionThresholdBytes,
+      protobuf_ports::kDefaultMemLevel,
       /*initialize_stats=*/nullptr);
 }
 
@@ -160,9 +171,10 @@
   for (int i = 0; i < num_of_documents; i++) {
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::PutResult put_result,
-        document_store->Put(CreateEmailDocument(
-            /*id=*/i, /*document_score=*/distribution(random_generator),
-            /*creation_timestamp_ms=*/1)));
+        document_store->Put(
+            document_util::CreateDocumentWrapper(CreateEmailDocument(
+                /*id=*/i, /*document_score=*/distribution(random_generator),
+                /*creation_timestamp_ms=*/1))));
     DocumentId document_id = put_result.new_document_id;
     doc_hit_infos.emplace_back(document_id);
   }
@@ -273,9 +285,10 @@
   for (int i = 0; i < num_of_documents; i++) {
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::PutResult put_result,
-        document_store->Put(CreateEmailDocument(
-            /*id=*/i, /*document_score=*/1,
-            /*creation_timestamp_ms=*/distribution(random_generator))));
+        document_store->Put(
+            document_util::CreateDocumentWrapper(CreateEmailDocument(
+                /*id=*/i, /*document_score=*/1,
+                /*creation_timestamp_ms=*/distribution(random_generator)))));
     DocumentId document_id = put_result.new_document_id;
     doc_hit_infos.emplace_back(document_id);
   }
@@ -382,8 +395,9 @@
   for (int i = 0; i < num_of_documents; i++) {
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::PutResult put_result,
-        document_store->Put(CreateEmailDocument(/*id=*/i, /*document_score=*/1,
-                                                /*creation_timestamp_ms=*/1)));
+        document_store->Put(document_util::CreateDocumentWrapper(
+            CreateEmailDocument(/*id=*/i, /*document_score=*/1,
+                                /*creation_timestamp_ms=*/1))));
     DocumentId document_id = put_result.new_document_id;
     doc_hit_infos.emplace_back(document_id);
   }
@@ -497,10 +511,10 @@
   for (int i = 0; i < num_of_documents; i++) {
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::PutResult put_result,
-        document_store->Put(CreateEmailDocument(
-                                /*id=*/i, /*document_score=*/1,
+        document_store->Put(CreateDocumentWrapper(
+            CreateEmailDocument(/*id=*/i, /*document_score=*/1,
                                 /*creation_timestamp_ms=*/1),
-                            /*num_tokens=*/10));
+            /*num_string_tokens=*/10)));
     DocumentId document_id = put_result.new_document_id;
     DocHitInfoTermFrequencyPair doc_hit =
         DocHitInfo(document_id, section_id_mask);
diff --git a/icing/scoring/scored-document-hits-ranker.h b/icing/scoring/scored-document-hits-ranker.h
index 2a7956d..9135ecb 100644
--- a/icing/scoring/scored-document-hits-ranker.h
+++ b/icing/scoring/scored-document-hits-ranker.h
@@ -25,7 +25,7 @@
 
 // TODO(sungyc): re-evaluate other similar implementations (e.g. std::sort +
 //               std::queue/std::vector). Also revisit the capacity shrinking
-//               issue for PopNext().
+//               issue for Pop().
 
 // ScoredDocumentHitsRanker is an interface class for ranking
 // ScoredDocumentHits.
@@ -33,19 +33,24 @@
  public:
   virtual ~ScoredDocumentHitsRanker() = default;
 
-  // Pop the next top JoinedScoredDocumentHit and return. It is undefined to
-  // call PopNext on an empty ranker, so the caller should check if it is not
-  // empty before calling.
+  // Pops the current element and moves to the next one.
+  //
+  // REQUIRES: !empty().
+  virtual void Pop() = 0;
+
+  // Returns the top JoinedScoredDocumentHit.
   //
   // Note: ranker may store ScoredDocumentHit or JoinedScoredDocumentHit. We can
   // add template for this interface, but since JoinedScoredDocumentHit is a
-  // superset of ScoredDocumentHit, we unify the return type of PopNext to use
-  // the superset type JoinedScoredDocumentHit in order to make it simple, and
+  // superset of ScoredDocumentHit, we unify the return type of Top to use the
+  // superset type JoinedScoredDocumentHit in order to make it simple, and
   // rankers storing ScoredDocumentHit should convert it to
   // JoinedScoredDocumentHit before returning. It makes the implementation
   // simpler, especially for ResultRetriever, which now only needs to deal with
   // one single return format.
-  virtual JoinedScoredDocumentHit PopNext() = 0;
+  //
+  // REQUIRES: !empty().
+  virtual const JoinedScoredDocumentHit& Top() const = 0;
 
   // Truncates the remaining ScoredDocumentHits to the given size. The best
   // ScoredDocumentHits (according to the ranking policy) should be kept.
diff --git a/icing/scoring/scorer_test.cc b/icing/scoring/scorer_test.cc
index 98ca18e..e849971 100644
--- a/icing/scoring/scorer_test.cc
+++ b/icing/scoring/scorer_test.cc
@@ -29,6 +29,7 @@
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/index/embed/embedding-query-results.h"
 #include "icing/index/hit/doc-hit-info.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/proto/scoring.pb.h"
@@ -43,6 +44,7 @@
 #include "icing/testing/fake-clock.h"
 #include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/document-util.h"
 
 namespace icing {
 namespace lib {
@@ -73,14 +75,18 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, doc_store_dir_, &fake_clock1_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, doc_store_dir_, &fake_clock1_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
 
     // Creates a simple email schema
@@ -193,8 +199,10 @@
           .SetCreationTimestampMs(fake_clock1().GetSystemTimeMilliseconds())
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store()->Put(test_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store()->Put(
+          document_util::CreateDocumentWrapper(test_document)));
   DocumentId document_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Scorer> scorer,
@@ -221,8 +229,10 @@
           .SetCreationTimestampMs(fake_clock2().GetSystemTimeMilliseconds())
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store()->Put(test_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store()->Put(
+          document_util::CreateDocumentWrapper(test_document)));
   DocumentId document_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Scorer> scorer,
@@ -251,8 +261,10 @@
           .SetCreationTimestampMs(fake_clock2().GetSystemTimeMilliseconds())
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store()->Put(test_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store()->Put(
+          document_util::CreateDocumentWrapper(test_document)));
   DocumentId document_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Scorer> scorer,
@@ -286,11 +298,15 @@
           .SetCreationTimestampMs(fake_clock2().GetSystemTimeMilliseconds())
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store()->Put(test_document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store()->Put(
+          document_util::CreateDocumentWrapper(test_document1)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store()->Put(test_document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store()->Put(
+          document_util::CreateDocumentWrapper(test_document2)));
   DocumentId document_id2 = put_result2.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Scorer> scorer,
@@ -320,8 +336,10 @@
           .SetCreationTimestampMs(fake_clock1().GetSystemTimeMilliseconds())
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store()->Put(test_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store()->Put(
+          document_util::CreateDocumentWrapper(test_document)));
   DocumentId document_id = put_result.new_document_id;
 
   // Create 3 scorers for 3 different usage types.
@@ -377,8 +395,10 @@
           .SetCreationTimestampMs(fake_clock1().GetSystemTimeMilliseconds())
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store()->Put(test_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store()->Put(
+          document_util::CreateDocumentWrapper(test_document)));
   DocumentId document_id = put_result.new_document_id;
 
   // Create 3 scorers for 3 different usage types.
@@ -434,8 +454,10 @@
           .SetCreationTimestampMs(fake_clock1().GetSystemTimeMilliseconds())
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store()->Put(test_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store()->Put(
+          document_util::CreateDocumentWrapper(test_document)));
   DocumentId document_id = put_result.new_document_id;
 
   // Create 3 scorers for 3 different usage types.
@@ -491,8 +513,10 @@
           .SetCreationTimestampMs(fake_clock1().GetSystemTimeMilliseconds())
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store()->Put(test_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store()->Put(
+          document_util::CreateDocumentWrapper(test_document)));
   DocumentId document_id = put_result.new_document_id;
 
   // Create 3 scorers for 3 different usage types.
@@ -570,8 +594,10 @@
           .SetCreationTimestampMs(fake_clock1().GetSystemTimeMilliseconds())
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store()->Put(test_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store()->Put(
+          document_util::CreateDocumentWrapper(test_document)));
   DocumentId document_id = put_result.new_document_id;
 
   // Create 3 scorers for 3 different usage types.
@@ -649,8 +675,10 @@
           .SetCreationTimestampMs(fake_clock1().GetSystemTimeMilliseconds())
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store()->Put(test_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store()->Put(
+          document_util::CreateDocumentWrapper(test_document)));
   DocumentId document_id = put_result.new_document_id;
 
   // Create 3 scorers for 3 different usage types.
@@ -764,8 +792,10 @@
           .SetCreationTimestampMs(fake_clock1().GetSystemTimeMilliseconds())
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             document_store()->Put(test_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store()->Put(
+          document_util::CreateDocumentWrapper(test_document)));
   DocumentId document_id = put_result.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
diff --git a/icing/scoring/scoring-processor.cc b/icing/scoring/scoring-processor.cc
index 4cba8a6..43f5d9b 100644
--- a/icing/scoring/scoring-processor.cc
+++ b/icing/scoring/scoring-processor.cc
@@ -110,6 +110,15 @@
         iterator_call_stats.num_leaf_advance_calls_main_index);
     search_stats->set_num_fetched_hits_integer_index(
         iterator_call_stats.num_leaf_advance_calls_integer_index);
+    search_stats->set_num_unquantized_embeddings_scored(
+        iterator_call_stats.embedding_stats.num_unquantized_embeddings_scored);
+    search_stats->set_num_quantized_embeddings_scored(
+        iterator_call_stats.embedding_stats.num_quantized_embeddings_scored);
+    search_stats->set_num_embedding_shards_read(static_cast<int32_t>(
+        iterator_call_stats.embedding_stats.unquantized_shards_read.size() +
+        iterator_call_stats.embedding_stats.quantized_shards_read.size()));
+    search_stats->set_num_embedding_bytes_read(
+        iterator_call_stats.embedding_stats.num_embedding_bytes_read);
   }
 
   return scored_document_hits;
diff --git a/icing/scoring/scoring-processor_test.cc b/icing/scoring/scoring-processor_test.cc
index f2dc84b..d94b054 100644
--- a/icing/scoring/scoring-processor_test.cc
+++ b/icing/scoring/scoring-processor_test.cc
@@ -33,6 +33,7 @@
 #include "icing/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator-test-util.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/proto/scoring.pb.h"
@@ -49,6 +50,7 @@
 #include "icing/testing/fake-clock.h"
 #include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/document-util.h"
 #include "icing/util/status-macros.h"
 
 namespace icing {
@@ -82,14 +84,18 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, doc_store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/true,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, doc_store_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/true,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
 
     // Creates a simple email schema
@@ -158,6 +164,12 @@
       .Build();
 }
 
+DocumentWrapper CreateDocumentWrapper(DocumentProto document,
+                                      int32_t num_string_tokens) {
+  document.mutable_internal_fields()->set_length_in_tokens(num_string_tokens);
+  return document_util::CreateDocumentWrapper(std::move(document));
+}
+
 libtextclassifier3::StatusOr<
     std::pair<std::vector<DocHitInfo>, std::vector<ScoredDocumentHit>>>
 CreateAndInsertsDocumentsWithScores(DocumentStore* document_store,
@@ -165,10 +177,11 @@
   std::vector<DocHitInfo> doc_hit_infos;
   std::vector<ScoredDocumentHit> scored_document_hits;
   for (int i = 0; i < scores.size(); i++) {
-    ICING_ASSIGN_OR_RETURN(DocumentStore::PutResult put_result,
-                           document_store->Put(CreateDocument(
-                               "icing", "email/" + std::to_string(i),
-                               scores.at(i), kDefaultCreationTimestampMs)));
+    ICING_ASSIGN_OR_RETURN(
+        DocumentStore::PutResult put_result,
+        document_store->Put(document_util::CreateDocumentWrapper(
+            CreateDocument("icing", "email/" + std::to_string(i), scores.at(i),
+                           kDefaultCreationTimestampMs))));
     DocumentId document_id = put_result.new_document_id;
     doc_hit_infos.emplace_back(document_id);
     scored_document_hits.emplace_back(document_id, kSectionIdMaskNone,
@@ -268,8 +281,8 @@
   // Sets up documents
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
-      document_store()->Put(CreateDocument("icing", "email/1", /*score=*/1,
-                                           kDefaultCreationTimestampMs)));
+      document_store()->Put(document_util::CreateDocumentWrapper(CreateDocument(
+          "icing", "email/1", /*score=*/1, kDefaultCreationTimestampMs))));
   DocumentId document_id1 = put_result1.new_document_id;
   DocHitInfo doc_hit_info1(document_id1);
 
@@ -379,17 +392,17 @@
       CreateDocument("icing", "email/3", kDefaultScore,
                      /*creation_timestamp_ms=*/kDefaultCreationTimestampMs);
 
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result1,
-      document_store()->Put(document1, /*num_tokens=*/10));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document1, /*num_string_tokens=*/10)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result2,
-      document_store()->Put(document2, /*num_tokens=*/100));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document2, /*num_string_tokens=*/100)));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result3,
-      document_store()->Put(document3, /*num_tokens=*/50));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document3, /*num_string_tokens=*/50)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   DocHitInfoTermFrequencyPair doc_hit_info1 = DocHitInfo(document_id1);
@@ -455,17 +468,17 @@
       CreateDocument("icing", "email/3", kDefaultScore,
                      /*creation_timestamp_ms=*/kDefaultCreationTimestampMs);
 
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result1,
-      document_store()->Put(document1, /*num_tokens=*/10));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document1, /*num_string_tokens=*/10)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result2,
-      document_store()->Put(document2, /*num_tokens=*/10));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document2, /*num_string_tokens=*/10)));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result3,
-      document_store()->Put(document3, /*num_tokens=*/10));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document3, /*num_string_tokens=*/10)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   DocHitInfoTermFrequencyPair doc_hit_info1 = DocHitInfo(document_id1);
@@ -530,17 +543,17 @@
       CreateDocument("icing", "email/3", kDefaultScore,
                      /*creation_timestamp_ms=*/kDefaultCreationTimestampMs);
 
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result1,
-      document_store()->Put(document1, /*num_tokens=*/10));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document1, /*num_string_tokens=*/10)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result2,
-      document_store()->Put(document2, /*num_tokens=*/10));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document2, /*num_string_tokens=*/10)));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result3,
-      document_store()->Put(document3, /*num_tokens=*/10));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document3, /*num_string_tokens=*/10)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   DocHitInfoTermFrequencyPair doc_hit_info1 = DocHitInfo(document_id1);
@@ -609,17 +622,17 @@
       CreateDocument("icing", "email/3", kDefaultScore,
                      /*creation_timestamp_ms=*/kDefaultCreationTimestampMs);
 
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result1,
-      document_store()->Put(document1, /*num_tokens=*/20));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document1, /*num_string_tokens=*/20)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result2,
-      document_store()->Put(document2, /*num_tokens=*/20));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document2, /*num_string_tokens=*/20)));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result3,
-      document_store()->Put(document3, /*num_tokens=*/20));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document3, /*num_string_tokens=*/20)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   // Index 5 terms with total frequencies:
@@ -830,9 +843,9 @@
       CreateDocument("icing", "email/1", kDefaultScore,
                      /*creation_timestamp_ms=*/kDefaultCreationTimestampMs);
 
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result1,
-      document_store()->Put(document1, /*num_tokens=*/10));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document1, /*num_string_tokens=*/10)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   // Document 1 contains the term "foo" 0 times in the "subject" property
@@ -883,13 +896,13 @@
       CreateDocument("icing", "email/2", kDefaultScore,
                      /*creation_timestamp_ms=*/kDefaultCreationTimestampMs);
 
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result1,
-      document_store()->Put(document1, /*num_tokens=*/1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document1, /*num_string_tokens=*/1)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result2,
-      document_store()->Put(document2, /*num_tokens=*/1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document2, /*num_string_tokens=*/1)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Document 1 contains the term "foo" 1 time in the "body" property
@@ -961,13 +974,13 @@
       CreateDocument("icing", "email/2", kDefaultScore,
                      /*creation_timestamp_ms=*/kDefaultCreationTimestampMs);
 
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result1,
-      document_store()->Put(document1, /*num_tokens=*/1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document1, /*num_string_tokens=*/1)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result2,
-      document_store()->Put(document2, /*num_tokens=*/1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document2, /*num_string_tokens=*/1)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Document 1 contains the term "foo" 1 time in the "body" property
@@ -1039,9 +1052,9 @@
       CreateDocument("icing", "email/2", kDefaultScore,
                      /*creation_timestamp_ms=*/kDefaultCreationTimestampMs);
 
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result1,
-      document_store()->Put(document1, /*num_tokens=*/1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document1, /*num_string_tokens=*/1)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   // Document 1 contains the term "foo" 1 time in the "body" property
@@ -1135,13 +1148,13 @@
       CreateDocument("icing", "email/2", kDefaultScore,
                      /*creation_timestamp_ms=*/kDefaultCreationTimestampMs);
 
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result1,
-      document_store()->Put(document1, /*num_tokens=*/1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document1, /*num_string_tokens=*/1)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result2,
-      document_store()->Put(document2, /*num_tokens=*/1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
+                             document_store()->Put(CreateDocumentWrapper(
+                                 document2, /*num_string_tokens=*/1)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   // Document 1 contains the term "foo" 1 time in the "body" property
@@ -1215,14 +1228,17 @@
       CreateDocument("icing", "email/3", kDefaultScore,
                      /*creation_timestamp_ms=*/1571100003333);
   // Intentionally inserts documents in a different order
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store()->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store()->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store()->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store()->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store()->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store()->Put(document_util::CreateDocumentWrapper(document3)));
   DocumentId document_id3 = put_result3.new_document_id;
   DocHitInfo doc_hit_info1(document_id1);
   DocHitInfo doc_hit_info2(document_id2);
@@ -1270,14 +1286,17 @@
       CreateDocument("icing", "email/3", kDefaultScore,
                      /*creation_timestamp_ms=*/kDefaultCreationTimestampMs);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store()->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store()->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store()->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store()->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store()->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store()->Put(document_util::CreateDocumentWrapper(document3)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   // Report usage for doc1 once and doc2 twice.
@@ -1337,14 +1356,17 @@
       CreateDocument("icing", "email/3", kDefaultScore,
                      /*creation_timestamp_ms=*/kDefaultCreationTimestampMs);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store()->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store()->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store()->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store()->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store()->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store()->Put(document_util::CreateDocumentWrapper(document3)));
   DocumentId document_id3 = put_result3.new_document_id;
 
   // Report usage for doc1 and doc2.
@@ -1442,13 +1464,16 @@
                                            kDefaultCreationTimestampMs);
 
   // Intentionally inserts documents in a different order
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store()->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store()->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             document_store()->Put(document3));
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store()->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store()->Put(document_util::CreateDocumentWrapper(document3)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store()->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
   DocumentId document_id3 = put_result3.new_document_id;
   DocHitInfo doc_hit_info1(document_id1);
diff --git a/icing/store/blob-store.cc b/icing/store/blob-store.cc
index eb3b686..8baeda0 100644
--- a/icing/store/blob-store.cc
+++ b/icing/store/blob-store.cc
@@ -141,9 +141,11 @@
 libtextclassifier3::StatusOr<BlobStore> BlobStore::Create(
     const Filesystem* filesystem, std::string base_dir, const Clock* clock,
     int64_t orphan_blob_time_to_live_ms, int32_t compression_level,
-    bool manage_blob_files) {
+    int32_t compression_mem_level, bool manage_blob_files,
+    const FeatureFlags* feature_flags) {
   ICING_RETURN_ERROR_IF_NULL(filesystem);
   ICING_RETURN_ERROR_IF_NULL(clock);
+  ICING_RETURN_ERROR_IF_NULL(feature_flags);
 
   // Make sure the blob file directory exists.
   if (!filesystem->CreateDirectoryRecursively(
@@ -173,19 +175,24 @@
       PortableFileBackedProtoLog<BlobInfoProto>::Create(
           filesystem, blob_info_proto_file_name,
           PortableFileBackedProtoLog<BlobInfoProto>::Options(
-              /*compress_in=*/true, constants::kMaxProtoSize,
-              compression_level)));
+              /*compress_in=*/true, constants::kMaxProtoSize, compression_level,
+              /*compression_threshold_bytes=*/0, compression_mem_level,
+              /*enable_smaller_decompression_buffer_size_in=*/false,
+              /*enable_proto_log_new_header_format_in=*/
+              true, feature_flags->enable_reusable_decompression_buffer())));
+  // TODO(b/435513415): pass feature flags object down to BlobStore and use it.
+  //   It is a remaining task to rollout new header format for proto log.
 
   std::unordered_map<std::string, int> blob_handle_to_offset;
   ICING_ASSIGN_OR_RETURN(
       blob_handle_to_offset,
       LoadBlobHandleToOffsetMapper(log_create_result.proto_log.get()));
 
-  return BlobStore(filesystem, std::move(base_dir), clock,
-                   orphan_blob_time_to_live_ms, compression_level,
-                   manage_blob_files, std::move(log_create_result.proto_log),
-                   std::move(blob_handle_to_offset),
-                   std::move(known_file_names));
+  return BlobStore(
+      filesystem, std::move(base_dir), clock, orphan_blob_time_to_live_ms,
+      compression_level, compression_mem_level, manage_blob_files,
+      std::move(log_create_result.proto_log), std::move(blob_handle_to_offset),
+      std::move(known_file_names));
 }
 
 BlobProto BlobStore::OpenWrite(
@@ -383,7 +390,7 @@
     while (prev_total_read_size < file_size) {
       int32_t size_to_read =
           std::min<int32_t>(kReadBufferSize, file_size - prev_total_read_size);
-      if (!filesystem_.Read(sfd.get(), buffer, size_to_read)) {
+      if (filesystem_.Read(sfd.get(), buffer, size_to_read) != size_to_read) {
         return CreateBlobProtoFromError(absl_ports::InternalError(
             absl_ports::StrCat("Failed to read blob file for handle: ",
                                blob_handle.digest())));
@@ -415,6 +422,50 @@
   return blob_proto;
 }
 
+BlobProto BlobStore::GetAllBlobInfos() {
+  BlobProto blob_proto;
+  blob_proto.mutable_status()->set_code(StatusProto::OK);
+  for (auto itr = blob_handle_to_offset_.begin();
+       itr != blob_handle_to_offset_.end(); ++itr) {
+    auto blob_info_proto_or = blob_info_log_->ReadProto(itr->second);
+    if (!blob_info_proto_or.ok()) {
+      continue;
+    }
+    *blob_proto.add_blob_info_protos() =
+        std::move(blob_info_proto_or).ValueOrDie();
+  }
+  return blob_proto;
+}
+
+BlobProto BlobStore::PutBlobInfos(const BlobProto& blob_proto) {
+  if (manage_blob_files_) {
+    ICING_LOG(ERROR)
+        << "Cannot put blob infos to blob store that manages blob files.";
+    return CreateBlobProtoFromError(absl_ports::FailedPreconditionError(
+        "Cannot put blob infos to blob store that manages blob files."));
+  }
+  BlobProto result_blob_proto;
+  result_blob_proto.mutable_status()->set_code(StatusProto::OK);
+  for (const auto& blob_info_proto : blob_proto.blob_info_protos()) {
+    std::string blob_handle_str =
+        BuildBlobHandleStr(blob_info_proto.blob_handle());
+    auto itr = blob_handle_to_offset_.find(blob_handle_str);
+    if (itr != blob_handle_to_offset_.end()) {
+      // The blob info proto already exists in the blob store.
+      continue;
+    }
+
+    auto blob_info_offset_or = blob_info_log_->WriteProto(blob_info_proto);
+    if (!blob_info_offset_or.ok()) {
+      ICING_LOG(ERROR) << blob_info_offset_or.status().error_message()
+                       << "Failed to write blob info";
+      return CreateBlobProtoFromError(blob_info_offset_or.status());
+    }
+    blob_handle_to_offset_[blob_handle_str] = blob_info_offset_or.ValueOrDie();
+  }
+  return result_blob_proto;
+}
+
 libtextclassifier3::Status BlobStore::PersistToDisk() {
   if (has_mutated_) {
     ICING_RETURN_IF_ERROR(blob_info_log_->PersistToDisk());
@@ -484,7 +535,8 @@
 }
 
 libtextclassifier3::StatusOr<std::vector<std::string>> BlobStore::Optimize(
-    const std::unordered_set<std::string>& dead_blob_handles) {
+    const std::unordered_set<std::string>& dead_blob_handles,
+    const FeatureFlags* feature_flags) {
   std::vector<std::string> blob_file_names_to_remove;
   blob_file_names_to_remove.reserve(dead_blob_handles.size());
 
@@ -496,13 +548,20 @@
         "Unable to delete temp file to prepare to build new blob proto file.");
   }
 
-  ICING_ASSIGN_OR_RETURN(PortableFileBackedProtoLog<BlobInfoProto>::CreateResult
-                             temp_log_create_result,
-                         PortableFileBackedProtoLog<BlobInfoProto>::Create(
-                             &filesystem_, temp_blob_info_proto_file_name,
-                             PortableFileBackedProtoLog<BlobInfoProto>::Options(
-                                 /*compress_in=*/true, constants::kMaxProtoSize,
-                                 compression_level_)));
+  ICING_ASSIGN_OR_RETURN(
+      PortableFileBackedProtoLog<BlobInfoProto>::CreateResult
+          temp_log_create_result,
+      PortableFileBackedProtoLog<BlobInfoProto>::Create(
+          &filesystem_, temp_blob_info_proto_file_name,
+          PortableFileBackedProtoLog<BlobInfoProto>::Options(
+              /*compress_in=*/true, constants::kMaxProtoSize,
+              compression_level_, /*compression_threshold_bytes=*/0,
+              compression_mem_level_,
+              /*enable_smaller_decompression_buffer_size_in=*/false,
+              /*enable_proto_log_new_header_format=*/true,
+              feature_flags->enable_reusable_decompression_buffer())));
+  // TODO(b/435513415): pass feature flags object down to BlobStore and use it.
+  //   It is a remaining task to rollout new header format for proto log.
   std::unique_ptr<PortableFileBackedProtoLog<BlobInfoProto>> new_blob_info_log =
       std::move(temp_log_create_result.proto_log);
 
@@ -565,7 +624,13 @@
           &filesystem_, old_blob_info_proto_file_name,
           PortableFileBackedProtoLog<BlobInfoProto>::Options(
               /*compress_in=*/true, constants::kMaxProtoSize,
-              compression_level_)));
+              compression_level_, /*compression_threshold_bytes=*/0,
+              compression_mem_level_,
+              /*enable_smaller_decompression_buffer_size_in=*/false,
+              /*enable_proto_log_new_header_format=*/true,
+              feature_flags->enable_reusable_decompression_buffer())));
+  // TODO(b/435513415): pass feature flags object down to BlobStore and use it.
+  //   It is a remaining task to rollout new header format for proto log.
   blob_info_log_ = std::move(log_create_result.proto_log);
   blob_handle_to_offset_ = std::move(new_blob_handle_to_offset);
   return blob_file_names_to_remove;
diff --git a/icing/store/blob-store.h b/icing/store/blob-store.h
index 77814a3..9864210 100644
--- a/icing/store/blob-store.h
+++ b/icing/store/blob-store.h
@@ -25,6 +25,7 @@
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/proto/blob.pb.h"
@@ -68,7 +69,8 @@
   static libtextclassifier3::StatusOr<BlobStore> Create(
       const Filesystem* filesystem, std::string base_dir, const Clock* clock,
       int64_t orphan_blob_time_to_live_ms, int32_t compression_level,
-      bool manage_blob_files);
+      int32_t compression_mem_level, bool manage_blob_files,
+      const FeatureFlags* feature_flags);
 
   // Gets or creates a file for write only purpose for the given blob handle.
   // To mark the blob is completed written, CommitBlob must be called. Once
@@ -142,6 +144,21 @@
   //   NotFoundError if the blob is not found
   BlobProto CommitBlob(const PropertyProto::BlobHandleProto& blob_handle);
 
+  // Gets all the blob info from the blob store.
+  //
+  // Returns:
+  //   BlobProto with all the blob info on success
+  //   InternalError on IO error
+  BlobProto GetAllBlobInfos();
+
+  // Puts the blob info protos from the blob proto to the blob info proto log
+  // file.
+  //
+  // Returns:
+  //   BlobProto with all the blob info on success
+  //   InternalError on IO error
+  BlobProto PutBlobInfos(const BlobProto& blob_proto);
+
   // Persists the blobs to disk.
   libtextclassifier3::Status PersistToDisk();
 
@@ -164,7 +181,8 @@
   //   manages blob files, this list will be empty.
   //   INTERNAL_ERROR on IO error
   libtextclassifier3::StatusOr<std::vector<std::string>> Optimize(
-      const std::unordered_set<std::string>& dead_blob_handles);
+      const std::unordered_set<std::string>& dead_blob_handles,
+      const FeatureFlags* feature_flags);
 
   // Calculates the StorageInfo for the Blob Store.
   //
@@ -178,7 +196,7 @@
   explicit BlobStore(
       const Filesystem* filesystem, std::string base_dir, const Clock* clock,
       int64_t orphan_blob_time_to_live_ms, int32_t compression_level,
-      bool manage_blob_files,
+      int32_t compression_mem_level, bool manage_blob_files,
       std::unique_ptr<PortableFileBackedProtoLog<BlobInfoProto>> blob_info_log,
       std::unordered_map<std::string, int32_t> blob_handle_to_offset,
       std::unordered_set<std::string> known_file_names)
@@ -187,6 +205,7 @@
         clock_(*clock),
         orphan_blob_time_to_live_ms_(orphan_blob_time_to_live_ms),
         compression_level_(compression_level),
+        compression_mem_level_(compression_mem_level),
         manage_blob_files_(manage_blob_files),
         blob_info_log_(std::move(blob_info_log)),
         blob_handle_to_offset_(std::move(blob_handle_to_offset)),
@@ -207,6 +226,7 @@
   const Clock& clock_;
   int64_t orphan_blob_time_to_live_ms_;
   int32_t compression_level_;
+  int32_t compression_mem_level_;
   bool manage_blob_files_;
 
   // The ground truth blob info log file, which is used to read/write/erase
diff --git a/icing/store/document-filter-data.h b/icing/store/document-filter-data.h
index 471ac60..789f45d 100644
--- a/icing/store/document-filter-data.h
+++ b/icing/store/document-filter-data.h
@@ -16,7 +16,6 @@
 #define ICING_STORE_DOCUMENT_FILTER_DATA_H_
 
 #include <cstdint>
-#include <type_traits>
 
 #include "icing/legacy/core/icing-packed-pod.h"
 #include "icing/store/namespace-id.h"
@@ -30,25 +29,23 @@
 class DocumentFilterData {
  public:
   explicit DocumentFilterData(NamespaceId namespace_id,
-                              uint64_t uri_fingerprint,
                               SchemaTypeId schema_type_id,
-                              int64_t expiration_timestamp_ms)
+                              int64_t expiration_timestamp_ms,
+                              int64_t raw_expiration_timestamp_ms)
       : expiration_timestamp_ms_(expiration_timestamp_ms),
-        uri_fingerprint_(uri_fingerprint),
+        raw_expiration_timestamp_ms_(raw_expiration_timestamp_ms),
         namespace_id_(namespace_id),
         schema_type_id_(schema_type_id) {}
 
   bool operator==(const DocumentFilterData& other) const {
     return namespace_id_ == other.namespace_id() &&
-           uri_fingerprint_ == other.uri_fingerprint() &&
            schema_type_id_ == other.schema_type_id() &&
-           expiration_timestamp_ms_ == other.expiration_timestamp_ms();
+           expiration_timestamp_ms_ == other.expiration_timestamp_ms() &&
+           raw_expiration_timestamp_ms_ == other.raw_expiration_timestamp_ms();
   }
 
   NamespaceId namespace_id() const { return namespace_id_; }
 
-  uint64_t uri_fingerprint() const { return uri_fingerprint_; }
-
   SchemaTypeId schema_type_id() const { return schema_type_id_; }
   void set_schema_type_id(SchemaTypeId schema_type_id) {
     schema_type_id_ = schema_type_id;
@@ -56,9 +53,31 @@
 
   int64_t expiration_timestamp_ms() const { return expiration_timestamp_ms_; }
 
+  int64_t raw_expiration_timestamp_ms() const {
+    return raw_expiration_timestamp_ms_;
+  }
+
  private:
+  // The expiration timestamp of the document in milliseconds. This is the min
+  // value of:
+  // - The document's raw expiration timestamp (see
+  //   raw_expiration_timestamp_ms_).
+  // - The propagated expiration timestamps according to its dependencies
+  //   specified in join properties with delete propagation enabled.
+  //
+  // Icing will use this value to:
+  // - Determine if a document is alive or expired.
+  // - Propagte to its dependents' expiration timestamps. Note that in most
+  //   cases the propagation should use this value instead of the raw expiration
+  //   timestamp.
   int64_t expiration_timestamp_ms_;
-  uint64_t uri_fingerprint_;
+
+  // The raw expiration timestamp of the document in milliseconds, calculated by
+  // the document's creation timestamp and ttl. This value is cached and mostly
+  // should only be used to reset expiration_timestamp_ms_ when Icing needs to
+  // recompute the propagated expiration timestamps of all documents.
+  int64_t raw_expiration_timestamp_ms_;
+
   NamespaceId namespace_id_;
   SchemaTypeId schema_type_id_;
 } __attribute__((packed));
diff --git a/icing/store/document-log-creator.cc b/icing/store/document-log-creator.cc
index 683e6e1..0354331 100644
--- a/icing/store/document-log-creator.cc
+++ b/icing/store/document-log-creator.cc
@@ -14,6 +14,7 @@
 
 #include "icing/store/document-log-creator.h"
 
+#include <cstdint>
 #include <memory>
 #include <string>
 #include <utility>
@@ -23,6 +24,7 @@
 #include "icing/absl_ports/annotate.h"
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/absl_ports/str_cat.h"
+#include "icing/feature-flags.h"
 #include "icing/file/constants.h"
 #include "icing/file/file-backed-proto-log.h"
 #include "icing/file/filesystem.h"
@@ -68,7 +70,10 @@
 libtextclassifier3::StatusOr<DocumentLogCreator::CreateResult>
 DocumentLogCreator::Create(const Filesystem* filesystem,
                            const std::string& base_dir,
-                           int32_t compression_level) {
+                           const FeatureFlags* feature_flags,
+                           int32_t compression_level,
+                           uint32_t compression_threshold_bytes,
+                           int32_t compression_mem_level) {
   bool v0_exists =
       filesystem->FileExists(MakeDocumentLogFilenameV0(base_dir).c_str());
   bool v1_exists =
@@ -77,8 +82,9 @@
   bool new_file = false;
   int preexisting_file_version = kCurrentVersion;
   if (v0_exists && !v1_exists) {
-    ICING_RETURN_IF_ERROR(
-        MigrateFromV0ToV1(filesystem, base_dir, compression_level));
+    ICING_RETURN_IF_ERROR(MigrateFromV0ToV1(
+        filesystem, base_dir, feature_flags, compression_level,
+        compression_threshold_bytes, compression_mem_level));
 
     // Need to regenerate derived files since documents may be written to a
     // different file offset in the log.
@@ -97,8 +103,11 @@
       PortableFileBackedProtoLog<DocumentWrapper>::Create(
           filesystem, MakeDocumentLogFilenameV1(base_dir),
           PortableFileBackedProtoLog<DocumentWrapper>::Options(
-              /*compress_in=*/true, constants::kMaxProtoSize,
-              compression_level)));
+              /*compress_in=*/true, constants::kMaxProtoSize, compression_level,
+              compression_threshold_bytes, compression_mem_level,
+              feature_flags->enable_smaller_decompression_buffer_size(),
+              feature_flags->enable_proto_log_new_header_format(),
+              feature_flags->enable_reusable_decompression_buffer())));
 
   CreateResult create_result = {std::move(log_create_result),
                                 preexisting_file_version, new_file};
@@ -107,7 +116,8 @@
 
 libtextclassifier3::Status DocumentLogCreator::MigrateFromV0ToV1(
     const Filesystem* filesystem, const std::string& base_dir,
-    int32_t compression_level) {
+    const FeatureFlags* feature_flags, int32_t compression_level,
+    uint32_t compression_threshold_bytes, int32_t compression_mem_level) {
   ICING_VLOG(1) << "Migrating from v0 to v1 document log.";
 
   // Our v0 proto log was non-portable, create it so we can read protos out from
@@ -132,9 +142,11 @@
           filesystem, MakeDocumentLogFilenameV1(base_dir),
           PortableFileBackedProtoLog<DocumentWrapper>::Options(
               /*compress_in=*/true,
-              /*max_proto_size_in=*/
-              constants::kMaxProtoSize,
-              /*compression_level_in=*/compression_level));
+              /*max_proto_size_in=*/constants::kMaxProtoSize, compression_level,
+              compression_threshold_bytes, compression_mem_level,
+              feature_flags->enable_smaller_decompression_buffer_size(),
+              feature_flags->enable_proto_log_new_header_format(),
+              feature_flags->enable_reusable_decompression_buffer()));
   if (!v1_create_result_or.ok()) {
     return absl_ports::Annotate(
         v1_create_result_or.status(),
diff --git a/icing/store/document-log-creator.h b/icing/store/document-log-creator.h
index 0c2794a..917e6c3 100644
--- a/icing/store/document-log-creator.h
+++ b/icing/store/document-log-creator.h
@@ -15,10 +15,12 @@
 #ifndef ICING_STORE_DOCUMENT_LOG_CREATOR_H_
 #define ICING_STORE_DOCUMENT_LOG_CREATOR_H_
 
+#include <cstdint>
 #include <string>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/proto/document_wrapper.pb.h"
@@ -58,7 +60,8 @@
   //   INTERNAL on any I/O error.
   static libtextclassifier3::StatusOr<DocumentLogCreator::CreateResult> Create(
       const Filesystem* filesystem, const std::string& base_dir,
-      int32_t compression_level);
+      const FeatureFlags* feature_flags, int32_t compression_level,
+      uint32_t compression_threshold_bytes, int32_t compression_mem_level);
 
   // Returns the filename of the document log, without any directory prefixes.
   // Used mainly for testing purposes.
@@ -76,7 +79,8 @@
   //   INTERNAL on I/O error.
   static libtextclassifier3::Status MigrateFromV0ToV1(
       const Filesystem* filesystem, const std::string& base_dir,
-      int32_t compression_level);
+      const FeatureFlags* feature_flags, int32_t compression_level,
+      uint32_t compression_threshold_bytes, int32_t compression_mem_level);
 };
 
 }  // namespace lib
diff --git a/icing/store/document-store.cc b/icing/store/document-store.cc
index da0ff4d..fce44d5 100644
--- a/icing/store/document-store.cc
+++ b/icing/store/document-store.cc
@@ -31,7 +31,6 @@
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/absl_ports/str_cat.h"
 #include "icing/feature-flags.h"
-#include "icing/file/file-backed-proto-log.h"
 #include "icing/file/file-backed-vector.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/memory-mapped-file-backed-proto-log.h"
@@ -68,10 +67,12 @@
 #include "icing/util/clock.h"
 #include "icing/util/crc32.h"
 #include "icing/util/data-loss.h"
+#include "icing/util/document-util.h"
 #include "icing/util/fingerprint-util.h"
 #include "icing/util/logging.h"
 #include "icing/util/scorable_property_set.h"
 #include "icing/util/status-macros.h"
+#include "icing/util/timestamp-util.h"
 #include "icing/util/tokenized-document.h"
 
 namespace icing {
@@ -110,12 +111,6 @@
 constexpr int32_t kNamespaceMapperMaxSize = 3 * 128 * 1024;  // 384 KiB
 constexpr int32_t kCorpusMapperMaxSize = 3 * 128 * 1024;     // 384 KiB
 
-DocumentWrapper CreateDocumentWrapper(DocumentProto&& document) {
-  DocumentWrapper document_wrapper;
-  *document_wrapper.mutable_document() = std::move(document);
-  return document_wrapper;
-}
-
 std::string MakeHeaderFilename(const std::string& base_dir) {
   return absl_ports::StrCat(base_dir, "/", kDocumentStoreHeaderFilename);
 }
@@ -156,27 +151,6 @@
   return absl_ports::StrCat(base_dir, "/", kCorpusIdMapperFilename);
 }
 
-int64_t CalculateExpirationTimestampMs(int64_t creation_timestamp_ms,
-                                       int64_t ttl_ms) {
-  if (ttl_ms == 0) {
-    // Special case where a TTL of 0 indicates the document should never
-    // expire. int64_t max, interpreted as seconds since epoch, represents
-    // some point in the year 292,277,026,596. So we're probably ok to use
-    // this as "never reaching this point".
-    return std::numeric_limits<int64_t>::max();
-  }
-
-  int64_t expiration_timestamp_ms;
-  if (__builtin_add_overflow(creation_timestamp_ms, ttl_ms,
-                             &expiration_timestamp_ms)) {
-    // Overflow detected. Treat overflow as the same behavior of just int64_t
-    // max
-    return std::numeric_limits<int64_t>::max();
-  }
-
-  return expiration_timestamp_ms;
-}
-
 InitializeStatsProto::RecoveryCause GetRecoveryCause(
     const DocumentLogCreator::CreateResult& create_result,
     bool force_recovery_and_revalidate_documents) {
@@ -287,13 +261,12 @@
 
 }  // namespace
 
-DocumentStore::DocumentStore(const Filesystem* filesystem,
-                             const std::string_view base_dir,
-                             const Clock* clock,
-                             const SchemaStore* schema_store,
-                             const FeatureFlags* feature_flags,
-                             bool pre_mapping_fbv, bool use_persistent_hash_map,
-                             int32_t compression_level)
+DocumentStore::DocumentStore(
+    const Filesystem* filesystem, const std::string_view base_dir,
+    const Clock* clock, const SchemaStore* schema_store,
+    const FeatureFlags* feature_flags, bool pre_mapping_fbv,
+    bool use_persistent_hash_map, int32_t compression_level,
+    uint32_t compression_threshold_bytes, int32_t compression_mem_level)
     : filesystem_(filesystem),
       base_dir_(base_dir),
       clock_(*clock),
@@ -302,19 +275,14 @@
       document_validator_(schema_store),
       pre_mapping_fbv_(pre_mapping_fbv),
       use_persistent_hash_map_(use_persistent_hash_map),
-      compression_level_(compression_level) {}
+      compression_level_(compression_level),
+      compression_threshold_bytes_(compression_threshold_bytes),
+      compression_mem_level_(compression_mem_level) {}
 
 libtextclassifier3::StatusOr<DocumentStore::PutResult> DocumentStore::Put(
-    const DocumentProto& document, int32_t num_tokens,
+    const DocumentWrapper& document_wrapper,
     PutDocumentStatsProto* put_document_stats) {
-  return Put(DocumentProto(document), num_tokens, put_document_stats);
-}
-
-libtextclassifier3::StatusOr<DocumentStore::PutResult> DocumentStore::Put(
-    DocumentProto&& document, int32_t num_tokens,
-    PutDocumentStatsProto* put_document_stats) {
-  document.mutable_internal_fields()->set_length_in_tokens(num_tokens);
-  return InternalPut(std::move(document), put_document_stats);
+  return InternalPut(document_wrapper, put_document_stats);
 }
 
 DocumentStore::~DocumentStore() {
@@ -332,6 +300,7 @@
     const FeatureFlags* feature_flags,
     bool force_recovery_and_revalidate_documents, bool pre_mapping_fbv,
     bool use_persistent_hash_map, int32_t compression_level,
+    uint32_t compression_threshold_bytes, int32_t compression_mem_level,
     InitializeStatsProto* initialize_stats) {
   ICING_RETURN_ERROR_IF_NULL(filesystem);
   ICING_RETURN_ERROR_IF_NULL(clock);
@@ -340,7 +309,8 @@
 
   auto document_store = std::unique_ptr<DocumentStore>(new DocumentStore(
       filesystem, base_dir, clock, schema_store, feature_flags, pre_mapping_fbv,
-      use_persistent_hash_map, compression_level));
+      use_persistent_hash_map, compression_level, compression_threshold_bytes,
+      compression_mem_level));
   ICING_ASSIGN_OR_RETURN(
       InitializeResult initialize_result,
       document_store->Initialize(force_recovery_and_revalidate_documents,
@@ -404,8 +374,9 @@
 libtextclassifier3::StatusOr<DocumentStore::InitializeResult>
 DocumentStore::Initialize(bool force_recovery_and_revalidate_documents,
                           InitializeStatsProto* initialize_stats) {
-  auto create_result_or =
-      DocumentLogCreator::Create(filesystem_, base_dir_, compression_level_);
+  auto create_result_or = DocumentLogCreator::Create(
+      filesystem_, base_dir_, &feature_flags_, compression_level_,
+      compression_threshold_bytes_, compression_mem_level_);
 
   // TODO(b/144458732): Implement a more robust version of TC_ASSIGN_OR_RETURN
   // that can support error logging.
@@ -499,15 +470,16 @@
   }
 
   DocumentStore::Header header;
-  if (!filesystem_->Read(MakeHeaderFilename(base_dir_).c_str(), &header,
-                         sizeof(header))) {
+  if (filesystem_->Read(MakeHeaderFilename(base_dir_).c_str(), &header,
+                        sizeof(header)) != sizeof(header)) {
     return absl_ports::InternalError(
         absl_ports::StrCat("Couldn't read: ", MakeHeaderFilename(base_dir_)));
   }
 
-  if (header.magic != DocumentStore::Header::kMagic) {
-    return absl_ports::InternalError(absl_ports::StrCat(
-        "Invalid header kMagic for file: ", MakeHeaderFilename(base_dir_)));
+  if (header.magic != Header::kMagic) {
+    ICING_LOG(ERROR) << "Invalid header magic for DocumentStore. Expected: "
+                     << Header::kMagic << ", actual: " << header.magic;
+    return absl_ports::InternalError("Invalid header magic for DocumentStore");
   }
 
   // TODO(b/144458732): Implement a more robust version of TC_ASSIGN_OR_RETURN
@@ -713,15 +685,20 @@
             scorable_property_cache_index,
             document_wrapper.document().internal_fields().length_in_tokens())));
 
-    int64_t expiration_timestamp_ms = CalculateExpirationTimestampMs(
-        document_wrapper.document().creation_timestamp_ms(),
-        document_wrapper.document().ttl_ms());
+    int64_t raw_expiration_timestamp_ms =
+        timestamp_util::CalculateRawExpirationTimestampMs(
+            document_wrapper.document().creation_timestamp_ms(),
+            document_wrapper.document().ttl_ms());
 
+    // When regenerating derived files, set the expiration timestamp as the raw
+    // expiration timestamp. The dependency should be evaluated by the caller
+    // and updated to the propagated expiration timestamp later.
     ICING_RETURN_IF_ERROR(UpdateFilterCache(
         new_document_id,
-        DocumentFilterData(namespace_id,
-                           new_doc_nsid_uri_fingerprint.fingerprint(),
-                           schema_type_id, expiration_timestamp_ms)));
+        DocumentFilterData(
+            namespace_id, schema_type_id,
+            /*expiration_timestamp_ms=*/raw_expiration_timestamp_ms,
+            raw_expiration_timestamp_ms)));
     iterator_status = iterator.Advance();
   }
 
@@ -1141,36 +1118,31 @@
 }
 
 libtextclassifier3::StatusOr<DocumentStore::PutResult>
-DocumentStore::InternalPut(DocumentProto&& document,
+DocumentStore::InternalPut(const DocumentWrapper& document_wrapper,
                            PutDocumentStatsProto* put_document_stats) {
   std::unique_ptr<Timer> put_timer = clock_.GetNewTimer();
-  ICING_RETURN_IF_ERROR(document_validator_.Validate(document));
 
   if (put_document_stats != nullptr) {
-    put_document_stats->set_document_size(document.ByteSizeLong());
+    put_document_stats->set_document_size(
+        document_wrapper.document().ByteSizeLong());
   }
 
   // Copy fields needed before they are moved
-  std::string name_space = document.namespace_();
-  std::string uri = document.uri();
-  std::string schema = document.schema();
-  int document_score = document.score();
-  int32_t length_in_tokens = document.internal_fields().length_in_tokens();
-  int64_t creation_timestamp_ms = document.creation_timestamp_ms();
-
-  // Sets the creation timestamp if caller hasn't specified.
-  if (document.creation_timestamp_ms() == 0) {
-    creation_timestamp_ms = clock_.GetSystemTimeMilliseconds();
-    document.set_creation_timestamp_ms(creation_timestamp_ms);
-  }
-
-  int64_t expiration_timestamp_ms =
-      CalculateExpirationTimestampMs(creation_timestamp_ms, document.ttl_ms());
+  std::string name_space = document_wrapper.document().namespace_();
+  std::string uri = document_wrapper.document().uri();
+  std::string schema = document_wrapper.document().schema();
+  int document_score = document_wrapper.document().score();
+  int32_t length_in_tokens =
+      document_wrapper.document().internal_fields().length_in_tokens();
+  int64_t creation_timestamp_ms =
+      document_wrapper.document().creation_timestamp_ms();
+  int64_t raw_expiration_timestamp_ms =
+      timestamp_util::CalculateRawExpirationTimestampMs(
+          creation_timestamp_ms, document_wrapper.document().ttl_ms());
 
   // Update ground truth first
   // TODO(b/144458732): Implement a more robust version of TC_ASSIGN_OR_RETURN
   // that can support error logging.
-  DocumentWrapper document_wrapper = CreateDocumentWrapper(std::move(document));
   auto offset_or = document_log_->WriteProto(document_wrapper);
   if (!offset_or.ok()) {
     ICING_LOG(ERROR) << offset_or.status().error_message()
@@ -1195,6 +1167,7 @@
   }
   PutResult put_result;
   put_result.new_document_id = new_document_id;
+  put_result.expiration_timestamp_ms = raw_expiration_timestamp_ms;
 
   // Update namespace maps
   ICING_ASSIGN_OR_RETURN(
@@ -1233,17 +1206,28 @@
                            corpus_id, document_score, creation_timestamp_ms,
                            scorable_property_cache_index, length_in_tokens)));
 
+  // Here we set the expiration timestamp as the raw expiration timestamp,
+  // because we haven't propagated the expiration timestamps yet and it should
+  // be done in the next step by the caller.
   ICING_RETURN_IF_ERROR(UpdateFilterCache(
       new_document_id,
-      DocumentFilterData(namespace_id,
-                         new_doc_nsid_uri_fingerprint.fingerprint(),
-                         schema_type_id, expiration_timestamp_ms)));
+      DocumentFilterData(
+          namespace_id, schema_type_id,
+          /*expiration_timestamp_ms=*/raw_expiration_timestamp_ms,
+          raw_expiration_timestamp_ms)));
 
   if (old_document_id_or.ok()) {
     // The old document exists, copy over the usage scores and delete the old
     // document.
     DocumentId old_document_id = old_document_id_or.ValueOrDie();
 
+    // Currently, old_document_id is only used by join index v3 to
+    // MigrateParent. old_document_id should be set even if the document is
+    // deleted or expired, but we flag guard it for extra safety.
+    if (feature_flags_.enable_non_existent_qualified_id_join()) {
+      put_result.old_document_id = old_document_id;
+    }
+
     ICING_RETURN_IF_ERROR(
         usage_store_->CloneUsageScores(/*from_document_id=*/old_document_id,
                                        /*to_document_id=*/new_document_id));
@@ -1253,9 +1237,9 @@
     auto delete_status =
         Delete(old_document_id, clock_.GetSystemTimeMilliseconds());
     if (delete_status.ok()) {
-      // The old document had existed and was not previously deleted. Return its
-      // document id to mark it as a replacement.
+      // The old document had existed and was not previously deleted.
       put_result.old_document_id = old_document_id;
+      put_result.was_replacement = true;
     } else if (!absl_ports::IsNotFound(delete_status)) {
       // Real error, pass it up.
       return delete_status;
@@ -1472,9 +1456,8 @@
   if (!filter_data_or.ok()) {
     // This would only happen if document_id is out of range of the
     // filter_cache, meaning we got some invalid document_id. Callers should
-    // already have checked that their document_id is valid or used
-    // DoesDocumentExist(WithStatus). Regardless, return std::nullopt since the
-    // document doesn't exist.
+    // already have checked the status or validated their document_id.
+    // Regardless, return std::nullopt since the document doesn't exist.
     return std::nullopt;
   }
 
@@ -1483,14 +1466,140 @@
   return std::move(filter_data_or).ValueOrDie();
 }
 
+bool DocumentStore::IsDocumentAlive(DocumentId document_id,
+                                    int64_t current_time_ms) const {
+  return GetAliveDocumentFilterData(document_id, current_time_ms).has_value();
+}
+
+bool DocumentStore::IsDocumentAlive(std::string_view name_space,
+                                    std::string_view uri,
+                                    int64_t current_time_ms) const {
+  auto document_id_or = GetDocumentId(name_space, uri);
+  if (!document_id_or.ok()) {
+    return false;
+  }
+  return IsDocumentAlive(document_id_or.ValueOrDie(), current_time_ms);
+}
+
+libtextclassifier3::StatusOr<
+    DocumentStore::UpdateDocumentExpirationTimestampResult>
+DocumentStore::UpdateDocumentExpirationTimestamp(
+    DocumentId document_id, int64_t expiration_timestamp_ms) {
+  if (document_id < 0 || document_id >= filter_cache_->num_elements()) {
+    return absl_ports::InvalidArgumentError(IcingStringUtil::StringPrintf(
+        "Document id '%d' invalid.", document_id));
+  }
+
+  std::optional<DocumentFilterData> filter_data =
+      GetNonDeletedDocumentFilterData(document_id);
+  if (!filter_data.has_value()) {
+    return absl_ports::NotFoundError(IcingStringUtil::StringPrintf(
+        "Document id '%d' is not alive", document_id));
+  }
+
+  // Currently we only support decreasing the expiration timestamp.
+  if (expiration_timestamp_ms < filter_data->expiration_timestamp_ms()) {
+    ICING_RETURN_IF_ERROR(filter_cache_->Set(
+        document_id, DocumentFilterData(
+                         filter_data->namespace_id(),
+                         filter_data->schema_type_id(), expiration_timestamp_ms,
+                         filter_data->raw_expiration_timestamp_ms())));
+    return UpdateDocumentExpirationTimestampResult{
+        .final_expiration_timestamp_ms = expiration_timestamp_ms,
+        .was_updated = true};
+  }
+  return UpdateDocumentExpirationTimestampResult{
+      .final_expiration_timestamp_ms = filter_data->expiration_timestamp_ms(),
+      .was_updated = false};
+}
+
+libtextclassifier3::Status
+DocumentStore::ResetAllAliveExpirationTimestampsToRaw(int64_t current_time_ms) {
+  for (DocumentId document_id = 0; document_id < filter_cache_->num_elements();
+       ++document_id) {
+    std::optional<DocumentFilterData> filter_data =
+        GetAliveDocumentFilterData(document_id, current_time_ms);
+    if (!filter_data.has_value()) {
+      continue;
+    }
+
+    if (filter_data->expiration_timestamp_ms() !=
+        filter_data->raw_expiration_timestamp_ms()) {
+      ICING_RETURN_IF_ERROR(filter_cache_->Set(
+          document_id,
+          DocumentFilterData(filter_data->namespace_id(),
+                             filter_data->schema_type_id(),
+                             /*expiration_timestamp_ms=*/
+                             filter_data->raw_expiration_timestamp_ms(),
+                             filter_data->raw_expiration_timestamp_ms())));
+    }
+  }
+  return libtextclassifier3::Status::OK;
+}
+
+libtextclassifier3::StatusOr<std::vector<DocumentStore::DocumentMetadata>>
+DocumentStore::PurgeExpiredDocuments(int64_t current_time_ms) {
+  std::vector<DocumentMetadata> deleted_doc_metadata_list;
+  for (DocumentId document_id = 0; document_id < filter_cache_->num_elements();
+       ++document_id) {
+    std::optional<DocumentFilterData> filter_data =
+        GetNonDeletedDocumentFilterData(document_id);
+    if (!filter_data.has_value()) {
+      continue;
+    }
+
+    // If the document is expired or expires within the threshold, delete it.
+    if (current_time_ms +
+            feature_flags_.expired_document_purge_threshold_ms() >=
+        filter_data->expiration_timestamp_ms()) {
+      // Expired but not deleted yet. Force delete it.
+      auto deleted_doc_metadata_or = ForceDelete(document_id);
+      if (!deleted_doc_metadata_or.ok()) {
+        if (absl_ports::IsNotFound(deleted_doc_metadata_or.status())) {
+          // This should not happen, but let's handle it here anyway.
+          continue;
+        }
+        // Real error. Return it.
+        return std::move(deleted_doc_metadata_or).status();
+      }
+      deleted_doc_metadata_list.push_back(
+          std::move(deleted_doc_metadata_or).ValueOrDie());
+    }
+  }
+  return deleted_doc_metadata_list;
+}
+
+int64_t DocumentStore::GetNextExpiredDocumentTimestampMs(
+    int64_t current_time_ms) {
+  int64_t next_expired_document_ts_ms = -1;
+  for (DocumentId document_id = 0; document_id < filter_cache_->num_elements();
+       ++document_id) {
+    std::optional<DocumentFilterData> filter_data =
+        GetAliveDocumentFilterData(document_id, current_time_ms);
+    if (!filter_data.has_value()) {
+      continue;
+    }
+
+    // Note: std::numeric_limits<int64_t>::max() is used to indicate that a
+    // document never expires, so we should avoid returning it.
+    if (filter_data->expiration_timestamp_ms() !=
+            std::numeric_limits<int64_t>::max() &&
+        (next_expired_document_ts_ms == -1 ||
+         filter_data->expiration_timestamp_ms() <
+             next_expired_document_ts_ms)) {
+      next_expired_document_ts_ms = filter_data->expiration_timestamp_ms();
+    }
+  }
+  return next_expired_document_ts_ms;
+}
+
 bool DocumentStore::IsDeleted(DocumentId document_id) const {
   auto file_offset_or = document_id_mapper_->Get(document_id);
   if (!file_offset_or.ok()) {
     // This would only happen if document_id is out of range of the
     // document_id_mapper, meaning we got some invalid document_id. Callers
-    // should already have checked that their document_id is valid or used
-    // DoesDocumentExist(WithStatus). Regardless, return true since the
-    // document doesn't exist.
+    // should already have checked the status or validated their document_id.
+    // Regardless, return true since the document doesn't exist.
     return true;
   }
   int64_t file_offset = *file_offset_or.ValueOrDie();
@@ -1506,9 +1615,8 @@
   if (!filter_data_or.ok()) {
     // This would only happen if document_id is out of range of the
     // filter_cache, meaning we got some invalid document_id. Callers should
-    // already have checked that their document_id is valid or used
-    // DoesDocumentExist(WithStatus). Regardless, return std::nullopt since the
-    // document doesn't exist.
+    // already have checked the status or validated their document_id.
+    // Regardless, return std::nullopt since the document doesn't exist.
     return std::nullopt;
   }
   DocumentFilterData document_filter_data = filter_data_or.ValueOrDie();
@@ -1560,6 +1668,46 @@
   return ClearDerivedData(document_id);
 }
 
+libtextclassifier3::StatusOr<DocumentStore::DocumentMetadata>
+DocumentStore::ForceDelete(DocumentId document_id) {
+  if (document_id < 0 || document_id >= document_id_mapper_->num_elements()) {
+    return absl_ports::InvalidArgumentError(IcingStringUtil::StringPrintf(
+        "Document id '%d' is invalid to force delete.", document_id));
+  }
+
+  auto document_log_offset_or = document_id_mapper_->Get(document_id);
+  if (!document_log_offset_or.ok()) {
+    return absl_ports::InternalError("Failed to find document offset.");
+  }
+  int64_t document_log_offset = *document_log_offset_or.ValueOrDie();
+
+  if (document_log_offset == kDocDeletedFlag) {
+    // Already deleted.
+    return absl_ports::NotFoundError(IcingStringUtil::StringPrintf(
+        "Document id '%d' was already deleted.", document_id));
+  }
+
+  // Read the document since we need to return DocumentMetadata.
+  auto document_wrapper_or = document_log_->ReadProto(document_log_offset);
+  if (!document_wrapper_or.ok()) {
+    ICING_LOG(ERROR) << document_wrapper_or.status().error_message()
+                     << "Failed to read from document log.";
+    return std::move(document_wrapper_or).status();
+  }
+  DocumentWrapper document_wrapper =
+      std::move(document_wrapper_or).ValueOrDie();
+
+  // Erases document proto.
+  ICING_RETURN_IF_ERROR(document_log_->EraseProto(document_log_offset));
+  ICING_RETURN_IF_ERROR(ClearDerivedData(document_id));
+
+  return DocumentMetadata{
+      .schema_type_name = document_wrapper.document().schema(),
+      .name_space = document_wrapper.document().namespace_(),
+      .uri = document_wrapper.document().uri(),
+      .document_id = document_id};
+}
+
 libtextclassifier3::StatusOr<NamespaceId> DocumentStore::GetNamespaceId(
     std::string_view name_space) const {
   return namespace_mapper_->Get(name_space);
@@ -1573,54 +1721,66 @@
   return corpus_mapper_->Get(corpus_nsid_schema_fp.EncodeToCString());
 }
 
-libtextclassifier3::StatusOr<int32_t> DocumentStore::GetResultGroupingEntryId(
+std::optional<int32_t> DocumentStore::GetResultGroupingEntryId(
     ResultSpecProto::ResultGroupingType result_group_type,
-    const std::string_view name_space, const std::string_view schema) const {
-  auto namespace_id = GetNamespaceId(name_space);
-  auto schema_type_id = schema_store_->GetSchemaTypeId(schema);
-  switch (result_group_type) {
-    case ResultSpecProto::NONE:
-      return absl_ports::InvalidArgumentError(
-          "Cannot group by ResultSpecProto::NONE");
-    case ResultSpecProto::SCHEMA_TYPE:
-      if (schema_type_id.ok()) {
-        return schema_type_id.ValueOrDie();
-      }
-      break;
-    case ResultSpecProto::NAMESPACE:
-      if (namespace_id.ok()) {
-        return namespace_id.ValueOrDie();
-      }
-      break;
-    case ResultSpecProto::NAMESPACE_AND_SCHEMA_TYPE:
-      if (namespace_id.ok() && schema_type_id.ok()) {
-        // TODO(b/258715421): Temporary workaround to get a
-        //                    ResultGroupingEntryId given the Namespace string
-        //                    and Schema string.
-        return namespace_id.ValueOrDie() << 16 | schema_type_id.ValueOrDie();
-      }
-      break;
-  }
-  return absl_ports::NotFoundError("Cannot generate ResultGrouping Entry Id");
+    std::string_view name_space, std::string_view schema) const {
+  auto namespace_id_or = GetNamespaceId(name_space);
+  auto schema_type_id_or = schema_store_->GetSchemaTypeId(schema);
+
+  NamespaceId namespace_id =
+      namespace_id_or.ok() ? namespace_id_or.ValueOrDie() : kInvalidNamespaceId;
+  SchemaTypeId schema_type_id = schema_type_id_or.ok()
+                                    ? schema_type_id_or.ValueOrDie()
+                                    : kInvalidSchemaTypeId;
+  return GetResultGroupingEntryId(result_group_type, namespace_id,
+                                  schema_type_id);
 }
 
-libtextclassifier3::StatusOr<int32_t> DocumentStore::GetResultGroupingEntryId(
+std::optional<int32_t> DocumentStore::GetResultGroupingEntryId(
     ResultSpecProto::ResultGroupingType result_group_type,
-    const NamespaceId namespace_id, const SchemaTypeId schema_type_id) const {
+    NamespaceId namespace_id, SchemaTypeId schema_type_id) const {
+  static_assert(sizeof(NamespaceId) * 8 <= 16,
+                "Current ResultGroupingEntryId encoding only supports "
+                "namespace id up to 16 bits.");
+  static_assert(sizeof(SchemaTypeId) * 8 <= 16,
+                "Current ResultGroupingEntryId encoding only supports schema "
+                "type id up to 16 bits.");
+
+  // Note: this encoding method only works for a single
+  // ResultSpecProto::ResultGroupingType in a single search request. If multiple
+  // types can be used in the same search request, this encoding method needs to
+  // be updated since there will be encoded id collisions for NAMESPACE and
+  // SCHEMA_TYPE.
+
   switch (result_group_type) {
-    case ResultSpecProto::NONE:
-      return absl_ports::InvalidArgumentError(
-          "Cannot group by ResultSpecProto::NONE");
-    case ResultSpecProto::SCHEMA_TYPE:
+    case ResultSpecProto::ResultGroupingType::
+        ResultSpecProto_ResultGroupingType_NONE:
+      return std::nullopt;
+    case ResultSpecProto::ResultGroupingType::
+        ResultSpecProto_ResultGroupingType_SCHEMA_TYPE: {
+      if (schema_type_id == kInvalidSchemaTypeId) {
+        return std::nullopt;
+      }
       return schema_type_id;
-    case ResultSpecProto::NAMESPACE:
+    }
+    case ResultSpecProto::ResultGroupingType::
+        ResultSpecProto_ResultGroupingType_NAMESPACE: {
+      if (namespace_id == kInvalidNamespaceId) {
+        return std::nullopt;
+      }
       return namespace_id;
-    case ResultSpecProto::NAMESPACE_AND_SCHEMA_TYPE:
+    }
+    case ResultSpecProto::ResultGroupingType::
+        ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE: {
+      if (namespace_id == kInvalidNamespaceId ||
+          schema_type_id == kInvalidSchemaTypeId) {
+        return std::nullopt;
+      }
       // TODO(b/258715421): Temporary workaround to get a ResultGroupingEntryId
       //                    given the Namespace Id and SchemaType Id.
-      return namespace_id << 16 | schema_type_id;
+      return (static_cast<int32_t>(namespace_id) << 16) | schema_type_id;
+    }
   }
-  return absl_ports::NotFoundError("Cannot generate ResultGrouping Entry Id");
 }
 
 libtextclassifier3::StatusOr<DocumentAssociatedScoreData>
@@ -1806,14 +1966,25 @@
 }
 
 libtextclassifier3::Status DocumentStore::PersistToDisk(
-    PersistType::Code persist_type) {
-  ICING_RETURN_IF_ERROR(document_log_->PersistToDisk());
+    PersistType::Code persist_type, PersistToDiskStatsProto* persist_stats) {
+  ICING_RETURN_IF_ERROR(document_log_->PersistToDisk(persist_stats, &clock_));
   if (persist_type == PersistType::LITE) {
     // only persist the document log.
     return libtextclassifier3::Status::OK;
   }
+
+  std::unique_ptr<Timer> overall_timer;
+
+  if (persist_stats) {
+    overall_timer = clock_.GetNewTimer();
+  }
   if (persist_type == PersistType::RECOVERY_PROOF) {
-    return UpdateChecksum().status();
+    libtextclassifier3::Status status = UpdateChecksum().status();
+    if (persist_stats) {
+      persist_stats->set_document_store_checksum_update_latency_ms(
+          overall_timer->GetElapsedMilliseconds());
+    }
+    return status;
   }
   ICING_RETURN_IF_ERROR(document_key_mapper_->PersistToDisk());
   ICING_RETURN_IF_ERROR(document_id_mapper_->PersistToDisk());
@@ -1825,8 +1996,18 @@
   ICING_RETURN_IF_ERROR(corpus_mapper_->PersistToDisk());
   ICING_RETURN_IF_ERROR(corpus_score_cache_->PersistToDisk());
 
+  if (persist_stats) {
+    persist_stats->set_document_store_components_persist_latency_ms(
+        overall_timer->GetElapsedMilliseconds());
+    overall_timer = clock_.GetNewTimer();
+  }
+
   // Update the combined checksum and write to header file.
   ICING_RETURN_IF_ERROR(UpdateChecksum());
+  if (persist_stats) {
+    persist_stats->set_document_store_checksum_update_latency_ms(
+        overall_timer->GetElapsedMilliseconds());
+  }
   return libtextclassifier3::Status::OK;
 }
 
@@ -1968,8 +2149,10 @@
   return CalculateDocumentStatusCounts(std::move(storage_info));
 }
 
-libtextclassifier3::Status DocumentStore::UpdateSchemaStore(
-    const SchemaStore* schema_store) {
+libtextclassifier3::StatusOr<DocumentStore::UpdateSchemaStoreResult>
+DocumentStore::UpdateSchemaStore(const SchemaStore* schema_store) {
+  UpdateSchemaStoreResult update_result;
+
   // Update all references to the SchemaStore
   schema_store_ = schema_store;
   document_validator_.UpdateSchemaStore(schema_store);
@@ -1990,7 +2173,7 @@
     }
 
     // Guaranteed to have a document now.
-    DocumentProto document = document_or.ValueOrDie();
+    DocumentProto document = std::move(document_or).ValueOrDie();
 
     // Revalidate that this document is still compatible
     if (document_validator_.Validate(document).ok()) {
@@ -2001,7 +2184,10 @@
           typename FileBackedVector<DocumentFilterData>::MutableView
               doc_filter_data_view,
           filter_cache_->GetMutable(document_id));
-      doc_filter_data_view.Get().set_schema_type_id(schema_type_id);
+      if (doc_filter_data_view.Get().schema_type_id() != schema_type_id) {
+        doc_filter_data_view.Get().set_schema_type_id(schema_type_id);
+        update_result.derived_files_changed = true;
+      }
     } else {
       // Document is no longer valid with the new SchemaStore. Mark as
       // deleted
@@ -2011,15 +2197,21 @@
         // Real error, pass up
         return delete_status;
       }
+
+      ++update_result.deleted_document_count;
+      update_result.derived_files_changed = true;
     }
   }
 
-  return libtextclassifier3::Status::OK;
+  return update_result;
 }
 
-libtextclassifier3::Status DocumentStore::OptimizedUpdateSchemaStore(
+libtextclassifier3::StatusOr<DocumentStore::UpdateSchemaStoreResult>
+DocumentStore::OptimizedUpdateSchemaStore(
     const SchemaStore* schema_store,
     const SchemaStore::SetSchemaResult& set_schema_result) {
+  UpdateSchemaStoreResult update_result;
+
   if (!set_schema_result.success) {
     // No new schema was set, no work to be done
     return libtextclassifier3::Status::OK;
@@ -2067,7 +2259,12 @@
             typename FileBackedVector<DocumentFilterData>::MutableView
                 doc_filter_data_view,
             filter_cache_->GetMutable(document_id));
+
+        // Since the old schema type id is present in
+        // old_schema_type_ids_changed, we know that (new) schema_type_id is
+        // different, so update it directly without checking.
         doc_filter_data_view.Get().set_schema_type_id(schema_type_id);
+        update_result.derived_files_changed = true;
       }
       if (revalidate_document) {
         delete_document = !document_validator_.Validate(document).ok();
@@ -2077,14 +2274,17 @@
     if (delete_document) {
       // Document is no longer valid with the new SchemaStore. Mark as deleted
       auto delete_status = Delete(document_id, current_time_ms);
-      if (!delete_status.ok() && !absl_ports::IsNotFound(delete_status)) {
+      if (delete_status.ok()) {
+        ++update_result.deleted_document_count;
+        update_result.derived_files_changed = true;
+      } else if (!absl_ports::IsNotFound(delete_status)) {
         // Real error, pass up
         return delete_status;
       }
     }
   }
 
-  return libtextclassifier3::Status::OK;
+  return update_result;
 }
 
 libtextclassifier3::Status DocumentStore::RegenerateScorablePropertyCache(
@@ -2125,11 +2325,6 @@
   return libtextclassifier3::Status::OK;
 }
 
-// TODO(b/121227117): Implement Optimize()
-libtextclassifier3::Status DocumentStore::Optimize() {
-  return libtextclassifier3::Status::OK;
-}
-
 libtextclassifier3::StatusOr<DocumentStore::OptimizeResult>
 DocumentStore::OptimizeInto(
     const std::string& new_directory, const LanguageSegmenter* lang_segmenter,
@@ -2147,6 +2342,7 @@
           filesystem_, new_directory, &clock_, schema_store_, &feature_flags_,
           /*force_recovery_and_revalidate_documents=*/false, pre_mapping_fbv_,
           use_persistent_hash_map_, compression_level_,
+          compression_threshold_bytes_, compression_mem_level_,
           /*initialize_stats=*/nullptr));
   std::unique_ptr<DocumentStore> new_doc_store =
       std::move(doc_store_create_result.document_store);
@@ -2178,23 +2374,42 @@
 
   int64_t current_time_ms = clock_.GetSystemTimeMilliseconds();
   for (DocumentId document_id = 0; document_id < document_cnt; document_id++) {
-    auto document_or = Get(document_id, /*clear_internal_fields=*/false);
-    if (absl_ports::IsNotFound(document_or.status())) {
+    libtextclassifier3::StatusOr<DocumentProto> document_or;
+    if (feature_flags_.enable_optimize_improvements()) {
       if (IsDeleted(document_id)) {
         ++num_deleted_documents;
+        continue;
       } else if (!GetNonExpiredDocumentFilterData(document_id,
                                                   current_time_ms)) {
         ++num_expired_documents;
+        continue;
       }
-      continue;
-    } else if (!document_or.ok()) {
-      // Real error, pass up
+      // The only expected errors for Get are for deleted
+      // or expired documents.
+      document_or = Get(document_id, /*clear_internal_fields=*/false);
+    } else {
+      document_or = Get(document_id, /*clear_internal_fields=*/false);
+      if (absl_ports::IsNotFound(document_or.status())) {
+        if (IsDeleted(document_id)) {
+          ++num_deleted_documents;
+        } else if (!GetNonExpiredDocumentFilterData(document_id,
+                                                    current_time_ms)) {
+          ++num_expired_documents;
+        }
+        continue;
+      }
+    }
+    if (!document_or.ok()) {
+      // Real error, pass up.
       return absl_ports::Annotate(
           document_or.status(),
           IcingStringUtil::StringPrintf(
               "Failed to retrieve Document for DocumentId %d", document_id));
     }
 
+    // TODO(b/405467475): add a new Get method to return DocumentWrapper and
+    // change document_to_keep to DocumentWrapper.
+
     // Guaranteed to have a document now.
     DocumentProto document_to_keep = std::move(document_or).ValueOrDie();
     // Remove blobs that still have reference are removed from the
@@ -2204,8 +2419,10 @@
 
     libtextclassifier3::StatusOr<PutResult> put_result_or;
     if (document_to_keep.internal_fields().length_in_tokens() == 0) {
+      // Create a TokenizedDocument. length_in_tokens will be set there.
       auto tokenized_document_or = TokenizedDocument::Create(
-          schema_store_, lang_segmenter, document_to_keep);
+          schema_store_, lang_segmenter, current_time_ms,
+          std::move(document_to_keep));
       if (!tokenized_document_or.ok()) {
         return absl_ports::Annotate(
             tokenized_document_or.status(),
@@ -2214,12 +2431,12 @@
       }
       TokenizedDocument tokenized_document(
           std::move(tokenized_document_or).ValueOrDie());
-      put_result_or = new_doc_store->Put(
-          std::move(document_to_keep), tokenized_document.num_string_tokens());
+      put_result_or = new_doc_store->Put(tokenized_document.document_wrapper());
     } else {
       // TODO(b/144458732): Implement a more robust version of
       // TC_ASSIGN_OR_RETURN that can support error logging.
-      put_result_or = new_doc_store->InternalPut(std::move(document_to_keep));
+      put_result_or = new_doc_store->InternalPut(
+          document_util::CreateDocumentWrapper(std::move(document_to_keep)));
     }
     if (!put_result_or.ok()) {
       ICING_LOG(ERROR) << put_result_or.status().error_message()
@@ -2397,10 +2614,9 @@
 
   // Resets the filter cache entry
   ICING_RETURN_IF_ERROR(UpdateFilterCache(
-      document_id,
-      DocumentFilterData(kInvalidNamespaceId, /*uri_fingerprint=*/0,
-                         kInvalidSchemaTypeId,
-                         /*expiration_timestamp_ms=*/-1)));
+      document_id, DocumentFilterData(kInvalidNamespaceId, kInvalidSchemaTypeId,
+                                      /*expiration_timestamp_ms=*/-1,
+                                      /*raw_expiration_timestamp_ms=*/-1)));
 
   // Clears the usage scores.
   return usage_store_->DeleteUsageScores(document_id);
diff --git a/icing/store/document-store.h b/icing/store/document-store.h
index 7246235..516d9db 100644
--- a/icing/store/document-store.h
+++ b/icing/store/document-store.h
@@ -16,6 +16,7 @@
 #define ICING_STORE_DOCUMENT_STORE_H_
 
 #include <cstdint>
+#include <limits>
 #include <memory>
 #include <optional>
 #include <string>
@@ -66,8 +67,8 @@
  public:
   struct Header {
     // Previously used magic numbers, please avoid reusing those:
-    // [0x1b99c8b0, 0x3e005b5e]
-    static constexpr int32_t kMagic = 0x8a32cd1f;
+    // [0x1b99c8b0, 0x3e005b5e, 0x8a32cd1f, 0x2eea71de]
+    static constexpr int32_t kMagic = 0x142f84cd;
 
     // Holds the magic as a quick sanity check against file corruption.
     int32_t magic;
@@ -115,6 +116,13 @@
     bool derived_files_regenerated;
   };
 
+  struct DocumentMetadata {
+    std::string schema_type_name;
+    std::string name_space;
+    std::string uri;
+    DocumentId document_id;
+  };
+
   // Not copyable
   DocumentStore(const DocumentStore&) = delete;
   DocumentStore& operator=(const DocumentStore&) = delete;
@@ -151,6 +159,7 @@
       const FeatureFlags* feature_flags,
       bool force_recovery_and_revalidate_documents, bool pre_mapping_fbv,
       bool use_persistent_hash_map, int32_t compression_level,
+      uint32_t compression_threshold_bytes, int32_t compression_mem_level,
       InitializeStatsProto* initialize_stats);
 
   // Discards all derived data in the document store.
@@ -176,7 +185,7 @@
   // of deleted or expired documents.
   int num_documents() const { return document_id_mapper_->num_elements(); }
 
-  // Puts the document into document store.
+  // Puts a single document into document store.
   //
   // If put_document_stats is present, the fields related to DocumentStore will
   // be populated.
@@ -193,16 +202,11 @@
   struct PutResult {
     DocumentId old_document_id = kInvalidDocumentId;
     DocumentId new_document_id = kInvalidDocumentId;
-
-    bool was_replacement() const {
-      return old_document_id != kInvalidDocumentId;
-    }
+    int64_t expiration_timestamp_ms = std::numeric_limits<int64_t>::max();
+    bool was_replacement = false;
   };
   libtextclassifier3::StatusOr<PutResult> Put(
-      const DocumentProto& document, int32_t num_tokens = 0,
-      PutDocumentStatsProto* put_document_stats = nullptr);
-  libtextclassifier3::StatusOr<PutResult> Put(
-      DocumentProto&& document, int32_t num_tokens = 0,
+      const DocumentWrapper& document_wrapper,
       PutDocumentStatsProto* put_document_stats = nullptr);
 
   // Finds and returns the document identified by the given key (namespace +
@@ -247,8 +251,10 @@
   // or expired). Order of namespaces is undefined.
   std::vector<std::string> GetAllNamespaces() const;
 
-  // Deletes the document identified by the given namespace and uri. The
-  // document proto will be erased immediately.
+  // TODO(b/384947619): migrate Delete APIs to return DocumentMetadata.
+
+  // Deletes the document identified by the given namespace and uri, only if it
+  // is still alive. The document proto will be erased immediately.
   //
   // NOTE:
   //    Space is not reclaimed for deleted documents until Optimize() is
@@ -262,8 +268,8 @@
                                     std::string_view uri,
                                     int64_t current_time_ms);
 
-  // Deletes the document identified by the given document_id. The document
-  // proto will be erased immediately.
+  // Deletes the document identified by the given document_id, only if it is
+  // still alive. The document proto will be erased immediately.
   //
   // NOTE:
   //    Space is not reclaimed for deleted documents until Optimize() is
@@ -277,6 +283,24 @@
   libtextclassifier3::Status Delete(DocumentId document_id,
                                     int64_t current_time_ms);
 
+  // Deletes the document identified by the given document_id. The document
+  // proto will be erased immediately.
+  //
+  // Different from Delete(), this method promises that the document proto will
+  // be erased even if the document is expired.
+  //
+  // NOTE:
+  //    Space is not reclaimed for deleted documents until Optimize() is
+  //    called.
+  //
+  // Returns:
+  //   DocumentMetadata of the deleted document on success
+  //   NOT_FOUND_ERROR if the document doesn't exist or has been deleted
+  //   INTERNAL_ERROR on IO error
+  //   INVALID_ARGUMENT_ERROR if document_id is invalid.
+  libtextclassifier3::StatusOr<DocumentMetadata> ForceDelete(
+      DocumentId document_id);
+
   // Returns the NamespaceId of the string namespace
   //
   // Returns:
@@ -289,9 +313,10 @@
   // Helper method to find a DocumentId that is associated with the given
   // namespace and uri.
   //
-  // NOTE: The DocumentId may refer to a invalid document (deleted
-  // or expired). Callers can call DoesDocumentExist(document_id) to ensure it
-  // refers to a valid Document.
+  // NOTE: if succeeded, it always returns a valid DocumentId, but this
+  // DocumentId may refer to a invalid document (deleted or expired). Callers
+  // can call GetAliveDocumentFilterData(document_id, current_time_ms) and check
+  // the return value to ensure it refers to an alive Document.
   //
   // Returns:
   //   A DocumentId on success
@@ -303,9 +328,10 @@
   // Helper method to find a DocumentId that is associated with the given
   // NamespaceIdFingerprint.
   //
-  // NOTE: The DocumentId may refer to a invalid document (deleted
-  // or expired). Callers can call DoesDocumentExist(document_id) to ensure it
-  // refers to a valid Document.
+  // NOTE: if succeeded, it always returns a valid DocumentId, but this
+  // DocumentId may refer to a invalid document (deleted or expired). Callers
+  // can call GetAliveDocumentFilterData(document_id, current_time_ms) and check
+  // the return value to ensure it refers to an alive Document.
   //
   // Returns:
   //   A DocumentId on success
@@ -328,30 +354,31 @@
   //
   // NOTE: ResultGroupingEntryIds that are generated by calls with different
   // ResultGroupingTypes should not be compared. Returned ResultGroupingEntryIds
-  // are only guarenteed to be unique within their own ResultGroupingType.
+  // are only guaranteed to be unique within their own ResultGroupingType.
   //
   // Returns:
-  //   A ResultGroupingEntryId on success
-  //   NOT_FOUND if the key doesn't exist
-  //   INTERNAL_ERROR on IO error
-  libtextclassifier3::StatusOr<int32_t> GetResultGroupingEntryId(
+  //   A valid ResultGroupingEntryId on success
+  //   std::nullopt there is no such result grouping entry id (e.g. result group
+  //     type is not supported, result group type is based on the given
+  //     namespace/schema but the namespace/schema doesn't exist).
+  std::optional<int32_t> GetResultGroupingEntryId(
       ResultSpecProto::ResultGroupingType result_group_type,
-      const std::string_view name_space, const std::string_view schema) const;
+      std::string_view name_space, std::string_view schema) const;
 
   // Returns the ResultGrouping Entry Id associated with the given NamespaceId
   // and SchemaTypeId
   //
   // NOTE: ResultGroupingEntryIds that are generated by calls with different
   // ResultGroupingTypes should not be compared. Returned ResultGroupingEntryIds
-  // are only guarenteed to be unique within their own ResultGroupingType.
+  // are only guaranteed to be unique within their own ResultGroupingType.
   //
   // Returns:
-  //   A ResultGroupingEntryId on success
-  //   NOT_FOUND if the key doesn't exist
-  //   INTERNAL_ERROR on IO error
-  libtextclassifier3::StatusOr<int32_t> GetResultGroupingEntryId(
+  //   A valid ResultGroupingEntryId on success
+  //   std::nullopt if there is no such result grouping entry id (result group
+  //     type is not supported).
+  std::optional<int32_t> GetResultGroupingEntryId(
       ResultSpecProto::ResultGroupingType result_group_type,
-      const NamespaceId namespace_id, const SchemaTypeId schema_type_id) const;
+      NamespaceId namespace_id, SchemaTypeId schema_type_id) const;
 
   // Returns the DocumentAssociatedScoreData of the document specified by the
   // DocumentId.
@@ -397,6 +424,74 @@
   std::optional<DocumentFilterData> GetNonDeletedDocumentFilterData(
       DocumentId document_id) const;
 
+  // Checks if the document (given document_id) is alive or not.
+  bool IsDocumentAlive(DocumentId document_id, int64_t current_time_ms) const;
+
+  // Checks if the document (given namespace, uri) is alive or not.
+  bool IsDocumentAlive(std::string_view name_space, std::string_view uri,
+                       int64_t current_time_ms) const;
+
+  // Updates the expiration timestamp of the given document only if the document
+  // is not deleted (note: expired document's expiration timestamp can be
+  // updated decreasingly).
+  //
+  // Currently we only allow to decrease the expiration timestamp. This prevents
+  // us from making an expired document alive again.
+  //
+  // The input expiration timestamp is usually the propagated expiration
+  // timestamp computed from the document dependencies (expiration and delete
+  // propagation).
+  //
+  // Returns:
+  //   On success, the final value of the expiration timestamp and a boolean
+  //     flag that indicates if the expiration timestamp is updated. If the
+  //     input expiration timestamp is not smaller than the current expiration
+  //     timestamp stored in DocumentFilterData cache, then it is no-op and the
+  //     current expiration timestamp will be returned.
+  //   NOT_FOUND_ERROR if the document is deleted
+  //   INVALID_ARGUMENT_ERROR if document_id is invalid
+  //   Any FileBackedVector error
+  struct UpdateDocumentExpirationTimestampResult {
+    // The final value of the expiration timestamp.
+    int64_t final_expiration_timestamp_ms;
+
+    // Whether the expiration timestamp was updated or not.
+    bool was_updated;
+  };
+  libtextclassifier3::StatusOr<UpdateDocumentExpirationTimestampResult>
+  UpdateDocumentExpirationTimestamp(DocumentId document_id,
+                                    int64_t expiration_timestamp_ms);
+
+  // Resets the expiration timestamp as the raw expiration timestamp of all
+  // alive documents. See DocumentFilterData for more details.
+  //
+  // Note: this is usually called before recomputing propagated expiration
+  // timestamps when Icing detects the document dependency (expiration and
+  // delete propagation) should be re-evaluated.
+  //
+  // Returns:
+  //   OK on success
+  //   Any FileBackedVector error
+  libtextclassifier3::Status ResetAllAliveExpirationTimestampsToRaw(
+      int64_t current_time_ms);
+
+  // Purges all expired documents given the current time.
+  //
+  // Note: documents that expire before
+  //   current_time_ms + feature_flags_.expired_document_purge_threshold_ms()
+  //   will also be purged.
+  //
+  // Returns:
+  //   A vector of purged expired document metadata on success
+  //   INTERNAL_ERROR on IO error
+  libtextclassifier3::StatusOr<std::vector<DocumentMetadata>>
+  PurgeExpiredDocuments(int64_t current_time_ms);
+
+  // Returns the expiration timestamp (in milliseconds) of the document that
+  // will expire next, relative to current_time_ms. If there are no more expired
+  // documents, returns -1.
+  int64_t GetNextExpiredDocumentTimestampMs(int64_t current_time_ms);
+
   // Gets the SchemaTypeId of a document.
   //
   // Returns:
@@ -461,7 +556,9 @@
   // Returns:
   //   OK on success
   //   INTERNAL on I/O error
-  libtextclassifier3::Status PersistToDisk(PersistType::Code persist_type);
+  libtextclassifier3::Status PersistToDisk(
+      PersistType::Code persist_type,
+      PersistToDiskStatsProto* persist_stats = nullptr);
 
   // Calculates the StorageInfo for the Document Store.
   //
@@ -469,6 +566,10 @@
   // that field will be set to -1.
   DocumentStorageInfoProto GetStorageInfo() const;
 
+  struct UpdateSchemaStoreResult {
+    int deleted_document_count = 0;
+    bool derived_files_changed = false;
+  };
   // Update any derived data off of the SchemaStore with the new SchemaStore.
   // This may include pointers, SchemaTypeIds, etc.
   //
@@ -482,18 +583,24 @@
   // OptimizedUpdateSchemaStore.
   //
   // Returns;
-  //   OK on success
+  //   UpdateSchemaStoreResult on success
   //   INTERNAL_ERROR on IO error
-  libtextclassifier3::Status UpdateSchemaStore(const SchemaStore* schema_store);
+  libtextclassifier3::StatusOr<UpdateSchemaStoreResult> UpdateSchemaStore(
+      const SchemaStore* schema_store);
 
-  // Performs the same funtionality as UpdateSchemaStore, but this can be more
+  // Performs the same functionality as UpdateSchemaStore, but this can be more
   // optimized in terms of less disk reads and less work if we know exactly
   // what's changed between the old and new SchemaStore.
   //
+  // NOTE: This function may delete documents. A document may be invalidated by
+  // the new SchemaStore, such as failing validation or having its schema type
+  // deleted from the schema.
+  //
   // Returns;
-  //   OK on success
+  //   UpdateSchemaStoreResult on success
   //   INTERNAL_ERROR on IO error
-  libtextclassifier3::Status OptimizedUpdateSchemaStore(
+  libtextclassifier3::StatusOr<UpdateSchemaStoreResult>
+  OptimizedUpdateSchemaStore(
       const SchemaStore* schema_store,
       const SchemaStore::SetSchemaResult& set_schema_result);
 
@@ -598,7 +705,9 @@
                          const SchemaStore* schema_store,
                          const FeatureFlags* feature_flags,
                          bool pre_mapping_fbv, bool use_persistent_hash_map,
-                         int32_t compression_level);
+                         int32_t compression_level,
+                         uint32_t compression_threshold_bytes,
+                         int32_t compression_mem_level);
 
   const Filesystem* const filesystem_;
   const std::string base_dir_;
@@ -622,6 +731,10 @@
   bool use_persistent_hash_map_;
 
   const int32_t compression_level_;
+  const uint32_t compression_threshold_bytes_;
+
+  // Level of memory usage for compression.
+  const int32_t compression_mem_level_;
 
   // A log used to store all documents, it serves as a ground truth of doc
   // store. key_mapper_ and document_id_mapper_ can be regenerated from it.
@@ -669,7 +782,7 @@
   std::unique_ptr<KeyMapper<NamespaceId>> namespace_mapper_;
 
   // Maps a corpus, i.e. a (namespace, schema type) pair, to a densely-assigned
-  // unique id. A coprus is assigned an
+  // unique id. A corpus is assigned an
   // id when the first document belonging to that corpus is added to the
   // DocumentStore. Corpus ids may be removed from the mapper during compaction.
   std::unique_ptr<
@@ -778,7 +891,7 @@
   bool HeaderExists();
 
   libtextclassifier3::StatusOr<PutResult> InternalPut(
-      DocumentProto&& document,
+      const DocumentWrapper& document_wrapper,
       PutDocumentStatsProto* put_document_stats = nullptr);
 
   // Helper function to do batch deletes. Documents with the given
@@ -811,32 +924,16 @@
   libtextclassifier3::StatusOr<CorpusAssociatedScoreData>
   GetCorpusAssociatedScoreDataToUpdate(CorpusId corpus_id) const;
 
-  // Check if a document exists. Existence means it hasn't been deleted and it
-  // hasn't expired yet.
-  //
-  // Returns:
-  //   OK if the document exists
-  //   INVALID_ARGUMENT if document_id is less than 0 or greater than the
-  //                    maximum value
-  //   NOT_FOUND if the document doesn't exist (i.e. deleted or expired)
-  //   INTERNAL_ERROR on IO error
-  libtextclassifier3::Status DoesDocumentExistWithStatus(
-      DocumentId document_id) const;
-
-  // Checks if a document has been deleted
+  // Checks if a document has been deleted.
   //
   // This is for internal-use only because we assume that the document_id is
-  // already valid. If you're unsure if the document_id is valid, use
-  // DoesDocumentExist(document_id) instead, which will perform those additional
-  // checks.
+  // already valid.
   bool IsDeleted(DocumentId document_id) const;
 
   // Checks if a document has expired.
   //
   // This is for internal-use only because we assume that the document_id is
-  // already valid. If you're unsure if the document_id is valid, use
-  // DoesDocumentExist(document_id) instead, which will perform those additional
-  // checks.
+  // already valid.
 
   // Returns:
   //   True:DocumentFilterData  if the given document isn't expired.
diff --git a/icing/store/document-store_benchmark.cc b/icing/store/document-store_benchmark.cc
index 3782da0..907c01b 100644
--- a/icing/store/document-store_benchmark.cc
+++ b/icing/store/document-store_benchmark.cc
@@ -14,26 +14,21 @@
 
 #include <unistd.h>
 
-#include <fstream>
-#include <iostream>
 #include <memory>
-#include <ostream>
 #include <random>
-#include <sstream>
-#include <stdexcept>
 #include <string>
 #include <string_view>
-#include <unordered_set>
-#include <vector>
+#include <utility>
 
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "testing/base/public/benchmark.h"
-#include "gmock/gmock.h"
-#include "gtest/gtest.h"
 #include "icing/document-builder.h"
 #include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/proto/document.pb.h"
+#include "icing/proto/document_wrapper.pb.h"
 #include "icing/proto/persist.pb.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/proto/term.pb.h"
@@ -44,6 +39,8 @@
 #include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 #include "icing/util/clock.h"
+#include "icing/util/document-util.h"
+#include "icing/util/logging.h"
 
 // Run on a Linux workstation:
 //    $ blaze build -c opt --dynamic_mode=off --copt=-gmlt
@@ -84,14 +81,15 @@
   std::string dir_;
 };
 
-DocumentProto CreateDocument(const std::string namespace_,
-                             const std::string uri) {
-  return DocumentBuilder()
-      .SetKey(namespace_, uri)
-      .SetSchema("email")
-      .AddStringProperty("subject", "subject foo")
-      .AddStringProperty("body", "body bar")
-      .Build();
+DocumentWrapper CreateDocument(const std::string namespace_,
+                               const std::string uri) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey(namespace_, uri)
+                               .SetSchema("email")
+                               .AddStringProperty("subject", "subject foo")
+                               .AddStringProperty("body", "body bar")
+                               .Build();
+  return document_util::CreateDocumentWrapper(std::move(document));
 }
 
 SchemaProto CreateSchema() {
@@ -138,6 +136,9 @@
       /*force_recovery_and_revalidate_documents=*/false,
       /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
       PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
+      PortableFileBackedProtoLog<
+          DocumentWrapper>::kDefaultCompressionThresholdBytes,
+      protobuf_ports::kDefaultMemLevel,
       /*initialize_stats=*/nullptr);
 }
 
@@ -204,7 +205,7 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  DocumentProto document = CreateDocument("namespace", "uri");
+  DocumentWrapper document = CreateDocument("namespace", "uri");
 
   for (auto s : state) {
     // It's ok that this is the same document over and over. We'll create a new
@@ -262,7 +263,7 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  DocumentProto document = CreateDocument("namespace", "uri");
+  DocumentWrapper document = CreateDocument("namespace", "uri");
 
   for (auto s : state) {
     state.PauseTiming();
@@ -298,7 +299,7 @@
     std::unique_ptr<DocumentStore> document_store =
         std::move(create_result.document_store);
 
-    DocumentProto document = CreateDocument("namespace", "uri");
+    DocumentWrapper document = CreateDocument("namespace", "uri");
     ICING_ASSERT_OK(document_store->Put(document));
     ICING_ASSERT_OK(document_store->PersistToDisk(PersistType::FULL));
   }
@@ -336,7 +337,7 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  DocumentProto document = CreateDocument("namespace", "uri");
+  DocumentWrapper document = CreateDocument("namespace", "uri");
   ICING_ASSERT_OK(document_store->Put(document));
   ICING_ASSERT_OK(document_store->PersistToDisk(PersistType::LITE));
 
diff --git a/icing/store/document-store_test.cc b/icing/store/document-store_test.cc
index a9cc0a5..ff531cf 100644
--- a/icing/store/document-store_test.cc
+++ b/icing/store/document-store_test.cc
@@ -24,7 +24,6 @@
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
-#include "icing/text_classifier/lib3/utils/hash/farmhash.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "icing/absl_ports/str_cat.h"
@@ -36,6 +35,7 @@
 #include "icing/file/mock-filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/portable/equals-proto.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/debug.pb.h"
 #include "icing/proto/document.pb.h"
@@ -66,6 +66,7 @@
 #include "icing/util/clock.h"
 #include "icing/util/crc32.h"
 #include "icing/util/data-loss.h"
+#include "icing/util/document-util.h"
 #include "icing/util/icu-data-file-helper.h"
 #include "icing/util/logging.h"
 #include "icing/util/scorable_property_set.h"
@@ -88,10 +89,20 @@
 using ::testing::IsTrue;
 using ::testing::Ne;
 using ::testing::Not;
+using ::testing::Optional;
 using ::testing::Pointee;
 using ::testing::Return;
 using ::testing::UnorderedElementsAre;
 
+using ResultSpecProto::ResultGroupingType::
+    ResultSpecProto_ResultGroupingType_NAMESPACE;
+using ResultSpecProto::ResultGroupingType::
+    ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE;
+using ResultSpecProto::ResultGroupingType::
+    ResultSpecProto_ResultGroupingType_NONE;
+using ResultSpecProto::ResultGroupingType::
+    ResultSpecProto_ResultGroupingType_SCHEMA_TYPE;
+
 const NamespaceStorageInfoProto& GetNamespaceStorageInfo(
     const DocumentStorageInfoProto& storage_info,
     const std::string& name_space) {
@@ -284,6 +295,9 @@
         /*force_recovery_and_revalidate_documents=*/false,
         GetParam().pre_mapping_fbv, GetParam().use_persistent_hash_map,
         PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
+        PortableFileBackedProtoLog<
+            DocumentWrapper>::kDefaultCompressionThresholdBytes,
+        protobuf_ports::kDefaultMemLevel,
         /*initialize_stats=*/nullptr);
   }
 
@@ -312,6 +326,12 @@
   const int64_t document2_expiration_timestamp_ = 3;  // creation + ttl
 };
 
+DocumentWrapper CreateDocumentWrapper(DocumentProto document,
+                                      int32_t num_string_tokens) {
+  document.mutable_internal_fields()->set_length_in_tokens(num_string_tokens);
+  return document_util::CreateDocumentWrapper(std::move(document));
+}
+
 TEST_P(DocumentStoreTest, CreationWithNullPointerShouldFail) {
   EXPECT_THAT(CreateDocumentStore(/*filesystem=*/nullptr, document_store_dir_,
                                   &fake_clock_, schema_store_.get()),
@@ -335,6 +355,33 @@
               StatusIs(libtextclassifier3::StatusCode::INTERNAL));
 }
 
+TEST_P(DocumentStoreTest, PutResult) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  // Put test_document1_ which never expires.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(document_util::CreateDocumentWrapper(test_document1_)));
+  EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result1.was_replacement);
+  EXPECT_THAT(put_result1.expiration_timestamp_ms,
+              Eq(document1_expiration_timestamp_));
+
+  // Put test_document2_.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(document_util::CreateDocumentWrapper(test_document2_)));
+  EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result2.was_replacement);
+  EXPECT_THAT(put_result2.expiration_timestamp_ms,
+              Eq(document2_expiration_timestamp_));
+}
+
 TEST_P(DocumentStoreTest, PutAndGetInSameNamespaceOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
@@ -344,15 +391,17 @@
       std::move(create_result.document_store);
 
   // Both documents have namespace of "icing"
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(test_document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(document_util::CreateDocumentWrapper(test_document1_)));
   EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result1.was_replacement());
+  EXPECT_FALSE(put_result1.was_replacement);
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(test_document2_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(document_util::CreateDocumentWrapper(test_document2_)));
   EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result2.was_replacement());
+  EXPECT_FALSE(put_result2.was_replacement);
   DocumentId document_id2 = put_result2.new_document_id;
 
   EXPECT_THAT(doc_store->Get(document_id1),
@@ -381,15 +430,17 @@
                                    .SetCreationTimestampMs(0)
                                    .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(foo_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(document_util::CreateDocumentWrapper(foo_document)));
   EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result1.was_replacement());
+  EXPECT_FALSE(put_result1.was_replacement);
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(DocumentProto(bar_document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(document_util::CreateDocumentWrapper(bar_document)));
   EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result2.was_replacement());
+  EXPECT_FALSE(put_result2.was_replacement);
   DocumentId document_id2 = put_result2.new_document_id;
 
   EXPECT_THAT(doc_store->Get(document_id1),
@@ -412,16 +463,18 @@
   DocumentProto document1 = DocumentProto(test_document1_);
   DocumentProto document2 = DocumentProto(test_document1_);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(document_util::CreateDocumentWrapper(document1)));
   EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result1.was_replacement());
+  EXPECT_FALSE(put_result1.was_replacement);
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
   EXPECT_THAT(put_result2.old_document_id, Eq(document_id1));
-  EXPECT_TRUE(put_result2.was_replacement());
+  EXPECT_TRUE(put_result2.was_replacement);
   EXPECT_THAT(document_id1, Not(document_id2));
   // document2 overrides document1, so document_id1 becomes invalid
   EXPECT_THAT(doc_store->Get(document_id1),
@@ -432,11 +485,12 @@
   // Makes sure that old doc ids are not getting reused.
   DocumentProto document3 = DocumentProto(test_document1_);
   document3.set_uri("another/uri/1");
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             doc_store->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      doc_store->Put(document_util::CreateDocumentWrapper(document3)));
   EXPECT_THAT(put_result3.old_document_id, Eq(kInvalidDocumentId));
   EXPECT_THAT(put_result3.new_document_id, Ne(document_id1));
-  EXPECT_FALSE(put_result3.was_replacement());
+  EXPECT_FALSE(put_result3.was_replacement);
 }
 
 TEST_P(DocumentStoreTest, IsDocumentExistingWithoutStatus) {
@@ -447,15 +501,17 @@
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(test_document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(document_util::CreateDocumentWrapper(test_document1_)));
   EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result1.was_replacement());
+  EXPECT_FALSE(put_result1.was_replacement);
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(test_document2_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(document_util::CreateDocumentWrapper(test_document2_)));
   EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result2.was_replacement());
+  EXPECT_FALSE(put_result2.was_replacement);
   DocumentId document_id2 = put_result2.new_document_id;
 
   EXPECT_TRUE(doc_store->GetAliveDocumentFilterData(
@@ -489,7 +545,8 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_EXPECT_OK(document_store->Put(DocumentProto(test_document1_)));
+  ICING_EXPECT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(test_document1_)));
   EXPECT_THAT(
       document_store->Get(test_document1_.namespace_(), test_document1_.uri()),
       IsOkAndHolds(EqualsProto(test_document1_)));
@@ -517,7 +574,8 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_EXPECT_OK(document_store->Put(document));
+  ICING_EXPECT_OK(
+      document_store->Put(document_util::CreateDocumentWrapper(document)));
   EXPECT_THAT(document_store->Get("namespace", "uri"),
               IsOkAndHolds(EqualsProto(document)));
 
@@ -545,10 +603,11 @@
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store->Put(test_document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(test_document1_)));
   EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result.was_replacement());
+  EXPECT_FALSE(put_result.was_replacement);
   DocumentId document_id = put_result.new_document_id;
 
   DocumentId invalid_document_id_negative = -1;
@@ -630,7 +689,8 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_EXPECT_OK(document_store->Put(test_document1_));
+  ICING_EXPECT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(test_document1_)));
 
   // First time is OK
   ICING_EXPECT_OK(document_store->Delete(
@@ -644,6 +704,109 @@
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 }
 
+TEST_P(DocumentStoreTest, ForceDelete) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> document_store =
+      std::move(create_result.document_store);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(test_document1_)));
+  DocumentId document_id = put_result.new_document_id;
+
+  // Sanity check that the document is alive.
+  ASSERT_TRUE(document_store->GetAliveDocumentFilterData(
+      document_id,
+      /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  ASSERT_TRUE(document_store->GetNonDeletedDocumentFilterData(document_id));
+
+  EXPECT_THAT(document_store->ForceDelete(document_id),
+              IsOkAndHolds(EqualsDocumentMetadata(
+                  test_document1_.schema(), test_document1_.namespace_(),
+                  test_document1_.uri(), document_id)));
+  EXPECT_FALSE(document_store->GetAliveDocumentFilterData(
+      document_id,
+      /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_FALSE(document_store->GetNonDeletedDocumentFilterData(document_id));
+}
+
+TEST_P(DocumentStoreTest, ForceDeleteAlreadyDeletedDocumentReturnsNotFound) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> document_store =
+      std::move(create_result.document_store);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(test_document1_)));
+  DocumentId document_id = put_result.new_document_id;
+
+  // Sanity check that the document is alive.
+  ASSERT_TRUE(document_store->GetAliveDocumentFilterData(
+      document_id,
+      /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  ASSERT_TRUE(document_store->GetNonDeletedDocumentFilterData(document_id));
+
+  // First time is OK
+  ICING_ASSERT_OK(document_store->ForceDelete(document_id));
+
+  // Deleting it again should get NOT_FOUND.
+  EXPECT_THAT(document_store->ForceDelete(document_id),
+              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
+}
+
+TEST_P(DocumentStoreTest, ForceDeleteExpiredDocumentOk) {
+  fake_clock_.SetSystemTimeMilliseconds(0);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> document_store =
+      std::move(create_result.document_store);
+
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace", "uri")
+                               .SetSchema("email")
+                               .SetCreationTimestampMs(0)
+                               .SetTtlMs(1000)
+                               .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store->Put(document_util::CreateDocumentWrapper(document)));
+  DocumentId document_id = put_result.new_document_id;
+
+  // Sanity check that the document is alive.
+  ASSERT_TRUE(document_store->GetAliveDocumentFilterData(
+      document_id,
+      /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  ASSERT_TRUE(document_store->GetNonDeletedDocumentFilterData(document_id));
+
+  // Adjust the clock to make the document expired.
+  fake_clock_.SetSystemTimeMilliseconds(2000);
+  ASSERT_FALSE(document_store->GetAliveDocumentFilterData(
+      document_id,
+      /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  ASSERT_TRUE(document_store->GetNonDeletedDocumentFilterData(document_id));
+
+  // Deleting an expired document is OK.
+  EXPECT_THAT(document_store->ForceDelete(document_id),
+              IsOkAndHolds(EqualsDocumentMetadata(
+                  document.schema(), document.namespace_(), document.uri(),
+                  document_id)));
+  EXPECT_FALSE(document_store->GetAliveDocumentFilterData(
+      document_id,
+      /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_FALSE(document_store->GetNonDeletedDocumentFilterData(document_id));
+}
+
 TEST_P(DocumentStoreTest, DeleteByNamespaceOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
@@ -655,22 +818,26 @@
   DocumentProto document1 = test_document1_;
   document1.set_namespace_("namespace.1");
   document1.set_uri("uri1");
-  ICING_ASSERT_OK(doc_store->Put(document1));
+  ICING_ASSERT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(document1)));
 
   DocumentProto document2 = test_document1_;
   document2.set_namespace_("namespace.2");
   document2.set_uri("uri1");
-  ICING_ASSERT_OK(doc_store->Put(document2));
+  ICING_ASSERT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(document2)));
 
   DocumentProto document3 = test_document1_;
   document3.set_namespace_("namespace.3");
   document3.set_uri("uri1");
-  ICING_ASSERT_OK(doc_store->Put(document3));
+  ICING_ASSERT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(document3)));
 
   DocumentProto document4 = test_document1_;
   document4.set_namespace_("namespace.1");
   document4.set_uri("uri2");
-  ICING_ASSERT_OK(doc_store->Put(document4));
+  ICING_ASSERT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(document4)));
 
   // DELETE namespace.1. document1 and document 4 should be deleted. document2
   // and document3 should still be retrievable.
@@ -721,7 +888,8 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_EXPECT_OK(document_store->Put(test_document1_));
+  ICING_EXPECT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(test_document1_)));
   ICING_EXPECT_OK(document_store->Delete(
       test_document1_.namespace_(), test_document1_.uri(),
       fake_clock_.GetSystemTimeMilliseconds()));
@@ -760,10 +928,14 @@
     std::unique_ptr<DocumentStore> doc_store =
         std::move(create_result.document_store);
 
-    ICING_ASSERT_OK(doc_store->Put(document1));
-    ICING_ASSERT_OK(doc_store->Put(document2));
-    ICING_ASSERT_OK(doc_store->Put(document3));
-    ICING_ASSERT_OK(doc_store->Put(document4));
+    ICING_ASSERT_OK(
+        doc_store->Put(document_util::CreateDocumentWrapper(document1)));
+    ICING_ASSERT_OK(
+        doc_store->Put(document_util::CreateDocumentWrapper(document2)));
+    ICING_ASSERT_OK(
+        doc_store->Put(document_util::CreateDocumentWrapper(document3)));
+    ICING_ASSERT_OK(
+        doc_store->Put(document_util::CreateDocumentWrapper(document4)));
 
     // DELETE namespace.1. document1 and document 4 should be deleted. document2
     // and document3 should still be retrievable.
@@ -835,10 +1007,12 @@
                                        .SetSchema("email")
                                        .SetCreationTimestampMs(1)
                                        .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult email_put_result1,
-                             document_store->Put(email_document_1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult email_put_result1,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(email_document_1)));
   EXPECT_THAT(email_put_result1.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(email_put_result1.was_replacement());
+  EXPECT_FALSE(email_put_result1.was_replacement);
   DocumentId email_1_document_id = email_put_result1.new_document_id;
 
   DocumentProto email_document_2 = DocumentBuilder()
@@ -846,10 +1020,12 @@
                                        .SetSchema("email")
                                        .SetCreationTimestampMs(1)
                                        .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult email_put_result2,
-                             document_store->Put(email_document_2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult email_put_result2,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(email_document_2)));
   EXPECT_THAT(email_put_result2.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(email_put_result2.was_replacement());
+  EXPECT_FALSE(email_put_result2.was_replacement);
   DocumentId email_2_document_id = email_put_result2.new_document_id;
 
   DocumentProto message_document = DocumentBuilder()
@@ -857,10 +1033,12 @@
                                        .SetSchema("message")
                                        .SetCreationTimestampMs(1)
                                        .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult message_put_result,
-                             document_store->Put(message_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult message_put_result,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(message_document)));
   EXPECT_THAT(message_put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(message_put_result.was_replacement());
+  EXPECT_FALSE(message_put_result.was_replacement);
   DocumentId message_document_id = message_put_result.new_document_id;
 
   DocumentProto person_document = DocumentBuilder()
@@ -868,10 +1046,12 @@
                                       .SetSchema("person")
                                       .SetCreationTimestampMs(1)
                                       .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult person_put_result,
-                             document_store->Put(person_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult person_put_result,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(person_document)));
   EXPECT_THAT(person_put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(person_put_result.was_replacement());
+  EXPECT_FALSE(person_put_result.was_replacement);
   DocumentId person_document_id = person_put_result.new_document_id;
 
   // Delete the "email" type and ensure that it works across both
@@ -937,7 +1117,8 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_EXPECT_OK(document_store->Put(test_document1_));
+  ICING_EXPECT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(test_document1_)));
   ICING_EXPECT_OK(document_store->Delete(
       test_document1_.namespace_(), test_document1_.uri(),
       fake_clock_.GetSystemTimeMilliseconds()));
@@ -988,15 +1169,19 @@
     std::unique_ptr<DocumentStore> document_store =
         std::move(create_result.document_store);
 
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult email_put_result,
-                               document_store->Put(email_document));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult email_put_result,
+        document_store->Put(
+            document_util::CreateDocumentWrapper(email_document)));
     EXPECT_THAT(email_put_result.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(email_put_result.was_replacement());
+    EXPECT_FALSE(email_put_result.was_replacement);
     email_document_id = email_put_result.new_document_id;
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult message_put_result,
-                               document_store->Put(message_document));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult message_put_result,
+        document_store->Put(
+            document_util::CreateDocumentWrapper(message_document)));
     EXPECT_THAT(message_put_result.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(message_put_result.was_replacement());
+    EXPECT_FALSE(message_put_result.was_replacement);
     message_document_id = message_put_result.new_document_id;
     // Delete "email". "message" documents should still be retrievable.
     DocumentStore::DeleteByGroupResult group_result =
@@ -1039,11 +1224,13 @@
                           schema_store_.get()));
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
-  ICING_EXPECT_OK(doc_store->Put(test_document1_));
+  ICING_EXPECT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(test_document1_)));
   ICING_EXPECT_OK(doc_store->Delete(test_document1_.namespace_(),
                                     test_document1_.uri(),
                                     fake_clock_.GetSystemTimeMilliseconds()));
-  ICING_EXPECT_OK(doc_store->Put(test_document1_));
+  ICING_EXPECT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(test_document1_)));
 }
 
 TEST_P(DocumentStoreTest, DeletedSchemaTypeFromSchemaStoreRecoversOk) {
@@ -1087,15 +1274,19 @@
     std::unique_ptr<DocumentStore> document_store =
         std::move(create_result.document_store);
 
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult email_put_result,
-                               document_store->Put(email_document));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult email_put_result,
+        document_store->Put(
+            document_util::CreateDocumentWrapper(email_document)));
     EXPECT_THAT(email_put_result.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(email_put_result.was_replacement());
+    EXPECT_FALSE(email_put_result.was_replacement);
     email_document_id = email_put_result.new_document_id;
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult message_put_result,
-                               document_store->Put(message_document));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult message_put_result,
+        document_store->Put(
+            document_util::CreateDocumentWrapper(message_document)));
     EXPECT_THAT(message_put_result.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(message_put_result.was_replacement());
+    EXPECT_FALSE(message_put_result.was_replacement);
     message_document_id = message_put_result.new_document_id;
 
     // Delete "email". "message" documents should still be retrievable.
@@ -1145,6 +1336,412 @@
               IsOkAndHolds(EqualsProto(message_document)));
 }
 
+TEST_P(DocumentStoreTest, PurgeExpiredDocuments) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> document_store =
+      std::move(create_result.document_store);
+
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetKey("namespace", "uri1")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(2000)
+                           .Build();
+  DocumentProto doc2 = DocumentBuilder()
+                           .SetKey("namespace", "uri2")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(1200)
+                           .Build();
+  DocumentProto doc3 = DocumentBuilder()
+                           .SetKey("namespace", "uri3")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(500)
+                           .Build();
+  DocumentProto doc4 = DocumentBuilder()
+                           .SetKey("namespace", "uri4")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(1800)
+                           .Build();
+  DocumentProto doc5 = DocumentBuilder()
+                           .SetKey("namespace", "uri5")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(1000)
+                           .Build();
+  // Put doc1 to doc5.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store->Put(document_util::CreateDocumentWrapper(doc1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store->Put(document_util::CreateDocumentWrapper(doc2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store->Put(document_util::CreateDocumentWrapper(doc3)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      document_store->Put(document_util::CreateDocumentWrapper(doc4)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result5,
+      document_store->Put(document_util::CreateDocumentWrapper(doc5)));
+  DocumentId doc_id1 = put_result1.new_document_id;
+  DocumentId doc_id2 = put_result2.new_document_id;
+  DocumentId doc_id3 = put_result3.new_document_id;
+  DocumentId doc_id4 = put_result4.new_document_id;
+  DocumentId doc_id5 = put_result5.new_document_id;
+
+  // Purge expired documents at t = 1200. doc2, doc3, doc5 should be purged.
+  EXPECT_THAT(document_store->PurgeExpiredDocuments(/*current_time_ms=*/1200),
+              IsOkAndHolds(ElementsAre(
+                  EqualsDocumentMetadata(doc2.schema(), doc2.namespace_(),
+                                         doc2.uri(), doc_id2),
+                  EqualsDocumentMetadata(doc3.schema(), doc3.namespace_(),
+                                         doc3.uri(), doc_id3),
+                  EqualsDocumentMetadata(doc5.schema(), doc5.namespace_(),
+                                         doc5.uri(), doc_id5))));
+  EXPECT_TRUE(document_store->GetNonDeletedDocumentFilterData(doc_id1));
+  EXPECT_FALSE(document_store->GetNonDeletedDocumentFilterData(doc_id2));
+  EXPECT_FALSE(document_store->GetNonDeletedDocumentFilterData(doc_id3));
+  EXPECT_TRUE(document_store->GetNonDeletedDocumentFilterData(doc_id4));
+  EXPECT_FALSE(document_store->GetNonDeletedDocumentFilterData(doc_id5));
+}
+
+TEST_P(DocumentStoreTest, PurgeExpiredDocuments_shouldSkipDeletedDocuments) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> document_store =
+      std::move(create_result.document_store);
+
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetKey("namespace", "uri1")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(2000)
+                           .Build();
+  DocumentProto doc2 = DocumentBuilder()
+                           .SetKey("namespace", "uri2")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(1200)
+                           .Build();
+  DocumentProto doc3 = DocumentBuilder()
+                           .SetKey("namespace", "uri3")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(500)
+                           .Build();
+  DocumentProto doc4 = DocumentBuilder()
+                           .SetKey("namespace", "uri4")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(1800)
+                           .Build();
+  DocumentProto doc5 = DocumentBuilder()
+                           .SetKey("namespace", "uri5")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(1000)
+                           .Build();
+  // Put doc1 to doc5.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store->Put(document_util::CreateDocumentWrapper(doc1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store->Put(document_util::CreateDocumentWrapper(doc2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store->Put(document_util::CreateDocumentWrapper(doc3)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      document_store->Put(document_util::CreateDocumentWrapper(doc4)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result5,
+      document_store->Put(document_util::CreateDocumentWrapper(doc5)));
+  DocumentId doc_id1 = put_result1.new_document_id;
+  DocumentId doc_id2 = put_result2.new_document_id;
+  DocumentId doc_id3 = put_result3.new_document_id;
+  DocumentId doc_id4 = put_result4.new_document_id;
+  DocumentId doc_id5 = put_result5.new_document_id;
+
+  // Delete doc3.
+  ICING_ASSERT_OK(document_store->Delete(doc3.namespace_(), doc3.uri(),
+                                         /*current_time_ms=*/0));
+
+  // Purge expired documents at t = 1200. doc2, doc5 should be purged. Since
+  // doc3 was already deleted, it should be skipped.
+  EXPECT_THAT(document_store->PurgeExpiredDocuments(/*current_time_ms=*/1200),
+              IsOkAndHolds(ElementsAre(
+                  EqualsDocumentMetadata(doc2.schema(), doc2.namespace_(),
+                                         doc2.uri(), doc_id2),
+                  EqualsDocumentMetadata(doc5.schema(), doc5.namespace_(),
+                                         doc5.uri(), doc_id5))));
+  EXPECT_TRUE(document_store->GetNonDeletedDocumentFilterData(doc_id1));
+  EXPECT_FALSE(document_store->GetNonDeletedDocumentFilterData(doc_id2));
+  EXPECT_FALSE(document_store->GetNonDeletedDocumentFilterData(doc_id3));
+  EXPECT_TRUE(document_store->GetNonDeletedDocumentFilterData(doc_id4));
+  EXPECT_FALSE(document_store->GetNonDeletedDocumentFilterData(doc_id5));
+}
+
+TEST_P(DocumentStoreTest,
+       PurgeExpiredDocuments_shouldPurgeDocumentsThatExpireWithinThreshold) {
+  auto custom_feature_flags = std::make_unique<FeatureFlags>(
+      /*enable_circular_schema_definitions=*/true,
+      /*enable_scorable_properties=*/true,
+      /*enable_embedding_quantization=*/true,
+      /*enable_repeated_field_joins=*/true,
+      /*enable_embedding_backup_generation=*/true,
+      /*enable_schema_database=*/true,
+      /*release_backup_schema_file_if_overlay_present=*/true,
+      /*enable_strict_page_byte_size_limit=*/true,
+      /*enable_smaller_decompression_buffer_size=*/true,
+      /*enable_eigen_embedding_scoring=*/true,
+      /*enable_passing_filter_to_children=*/true,
+      /*enable_proto_log_new_header_format=*/true,
+      /*enable_embedding_iterator_v2=*/true,
+      /*enable_reusable_decompression_buffer=*/true,
+      /*enable_schema_type_id_optimization=*/true,
+      /*enable_optimize_improvements=*/true,
+      /*expired_document_purge_threshold_ms=*/1000,  // 1 second
+      /*enable_non_existent_qualified_id_join=*/true,
+      /*enable_skip_set_schema_type_equality_check=*/true,
+      /*enable_embed_query_optimization=*/true,
+      /*enable_schema_definition_deduping=*/true);
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      DocumentStore::Create(
+          &filesystem_, document_store_dir_, &fake_clock_, schema_store_.get(),
+          custom_feature_flags.get(),
+          /*force_recovery_and_revalidate_documents=*/false,
+          GetParam().pre_mapping_fbv, GetParam().use_persistent_hash_map,
+          PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
+          PortableFileBackedProtoLog<
+              DocumentWrapper>::kDefaultCompressionThresholdBytes,
+          protobuf_ports::kDefaultMemLevel,
+          /*initialize_stats=*/nullptr));
+
+  std::unique_ptr<DocumentStore> document_store =
+      std::move(create_result.document_store);
+
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetKey("namespace", "uri1")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(2000)
+                           .Build();
+  DocumentProto doc2 = DocumentBuilder()
+                           .SetKey("namespace", "uri2")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(3000)
+                           .Build();
+  DocumentProto doc3 = DocumentBuilder()
+                           .SetKey("namespace", "uri3")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(3001)
+                           .Build();
+  DocumentProto doc4 = DocumentBuilder()
+                           .SetKey("namespace", "uri4")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(1200)
+                           .Build();
+  // Put doc1 to doc4.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store->Put(document_util::CreateDocumentWrapper(doc1)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store->Put(document_util::CreateDocumentWrapper(doc2)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      document_store->Put(document_util::CreateDocumentWrapper(doc3)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      document_store->Put(document_util::CreateDocumentWrapper(doc4)));
+  DocumentId doc_id1 = put_result1.new_document_id;
+  DocumentId doc_id2 = put_result2.new_document_id;
+  DocumentId doc_id3 = put_result3.new_document_id;
+  DocumentId doc_id4 = put_result4.new_document_id;
+
+  // Purge expired documents at t = 2000. doc1, doc2, doc4 should be purged.
+  // Doc3 should still be alive.
+  EXPECT_THAT(document_store->PurgeExpiredDocuments(/*current_time_ms=*/2000),
+              IsOkAndHolds(ElementsAre(
+                  EqualsDocumentMetadata(doc1.schema(), doc1.namespace_(),
+                                         doc1.uri(), doc_id1),
+                  EqualsDocumentMetadata(doc2.schema(), doc2.namespace_(),
+                                         doc2.uri(), doc_id2),
+                  EqualsDocumentMetadata(doc4.schema(), doc4.namespace_(),
+                                         doc4.uri(), doc_id4))));
+  EXPECT_FALSE(document_store->GetNonDeletedDocumentFilterData(doc_id1));
+  EXPECT_FALSE(document_store->GetNonDeletedDocumentFilterData(doc_id2));
+  EXPECT_TRUE(document_store->GetNonDeletedDocumentFilterData(doc_id3));
+  EXPECT_FALSE(document_store->GetNonDeletedDocumentFilterData(doc_id4));
+}
+
+TEST_P(DocumentStoreTest, GetNextExpiredDocumentTimestampMs) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> document_store =
+      std::move(create_result.document_store);
+
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetKey("namespace", "uri1")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(2000)  // Expires at 2000 ms.
+                           .Build();
+  DocumentProto doc2 = DocumentBuilder()
+                           .SetKey("namespace", "uri2")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(1200)  // Expires at 1200 ms.
+                           .Build();
+  DocumentProto doc3 = DocumentBuilder()
+                           .SetKey("namespace", "uri3")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(500)  // Expires at 500 ms.
+                           .Build();
+  DocumentProto doc4 = DocumentBuilder()
+                           .SetKey("namespace", "uri4")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(1800)  // Expires at 1800 ms.
+                           .Build();
+  DocumentProto doc5 = DocumentBuilder()
+                           .SetKey("namespace", "uri5")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(1000)  // Expires at 1000 ms.
+                           .Build();
+  // Put doc1 to doc5.
+  ICING_ASSERT_OK(
+      document_store->Put(document_util::CreateDocumentWrapper(doc1)));
+  ICING_ASSERT_OK(
+      document_store->Put(document_util::CreateDocumentWrapper(doc2)));
+  ICING_ASSERT_OK(
+      document_store->Put(document_util::CreateDocumentWrapper(doc3)));
+  ICING_ASSERT_OK(
+      document_store->Put(document_util::CreateDocumentWrapper(doc4)));
+  ICING_ASSERT_OK(
+      document_store->Put(document_util::CreateDocumentWrapper(doc5)));
+
+  EXPECT_THAT(
+      document_store->GetNextExpiredDocumentTimestampMs(/*current_time_ms=*/0),
+      Eq(500));
+  EXPECT_THAT(document_store->GetNextExpiredDocumentTimestampMs(
+                  /*current_time_ms=*/500),
+              Eq(1000));
+  EXPECT_THAT(document_store->GetNextExpiredDocumentTimestampMs(
+                  /*current_time_ms=*/1000),
+              Eq(1200));
+  EXPECT_THAT(document_store->GetNextExpiredDocumentTimestampMs(
+                  /*current_time_ms=*/1200),
+              Eq(1800));
+  EXPECT_THAT(document_store->GetNextExpiredDocumentTimestampMs(
+                  /*current_time_ms=*/1800),
+              Eq(2000));
+  EXPECT_THAT(document_store->GetNextExpiredDocumentTimestampMs(
+                  /*current_time_ms=*/2000),
+              Eq(-1));
+}
+
+TEST_P(DocumentStoreTest,
+       GetNextExpiredDocumentTimestampMs_shouldSkipDeletedDocuments) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> document_store =
+      std::move(create_result.document_store);
+
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetKey("namespace", "uri1")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(2000)  // Expires at 2000 ms.
+                           .Build();
+  DocumentProto doc2 = DocumentBuilder()
+                           .SetKey("namespace", "uri2")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(500)  // Expires at 500 ms.
+                           .Build();
+  DocumentProto doc3 = DocumentBuilder()
+                           .SetKey("namespace", "uri3")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(1200)  // Expires at 1200 ms.
+                           .Build();
+
+  // Put doc1 to doc3.
+  ICING_ASSERT_OK(
+      document_store->Put(document_util::CreateDocumentWrapper(doc1)));
+  ICING_ASSERT_OK(
+      document_store->Put(document_util::CreateDocumentWrapper(doc2)));
+  ICING_ASSERT_OK(
+      document_store->Put(document_util::CreateDocumentWrapper(doc3)));
+
+  EXPECT_THAT(
+      document_store->GetNextExpiredDocumentTimestampMs(/*current_time_ms=*/0),
+      Eq(500));
+
+  // Delete doc2 and get the next expired document timestamp again with current
+  // time = 0.
+  ICING_ASSERT_OK(
+      document_store->Delete("namespace", "uri2", /*current_time_ms=*/0));
+  EXPECT_THAT(
+      document_store->GetNextExpiredDocumentTimestampMs(/*current_time_ms=*/0),
+      Eq(1200));
+}
+
+TEST_P(
+    DocumentStoreTest,
+    GetNextExpiredDocumentTimestampMs_shouldReturnNegativeOneIfNoDocumentExpires) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> document_store =
+      std::move(create_result.document_store);
+
+  DocumentProto doc1 = DocumentBuilder()
+                           .SetKey("namespace", "uri1")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(0)  // Never expires.
+                           .Build();
+  DocumentProto doc2 = DocumentBuilder()
+                           .SetKey("namespace", "uri2")
+                           .SetSchema("email")
+                           .SetCreationTimestampMs(0)
+                           .SetTtlMs(0)  // Never expires.
+                           .Build();
+
+  // Put doc1 to doc5.
+  ICING_ASSERT_OK(
+      document_store->Put(document_util::CreateDocumentWrapper(doc1)));
+  ICING_ASSERT_OK(
+      document_store->Put(document_util::CreateDocumentWrapper(doc2)));
+
+  EXPECT_THAT(
+      document_store->GetNextExpiredDocumentTimestampMs(/*current_time_ms=*/0),
+      Eq(-1));
+}
+
 TEST_P(DocumentStoreTest, OptimizeIntoSingleNamespace) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
@@ -1177,9 +1774,12 @@
   // Nothing should have expired yet.
   fake_clock_.SetSystemTimeMilliseconds(100);
 
-  ICING_ASSERT_OK(doc_store->Put(document1));
-  ICING_ASSERT_OK(doc_store->Put(document2));
-  ICING_ASSERT_OK(doc_store->Put(document3));
+  ICING_ASSERT_OK(doc_store->Put(
+      CreateDocumentWrapper(document1, /*num_string_tokens=*/0)));
+  ICING_ASSERT_OK(doc_store->Put(
+      CreateDocumentWrapper(document2, /*num_string_tokens=*/0)));
+  ICING_ASSERT_OK(doc_store->Put(
+      CreateDocumentWrapper(document3, /*num_string_tokens=*/0)));
 
   std::string original_document_log = absl_ports::StrCat(
       document_store_dir_, "/", DocumentLogCreator::GetDocumentLogFilename());
@@ -1326,11 +1926,16 @@
   // Nothing should have expired yet.
   fake_clock_.SetSystemTimeMilliseconds(100);
 
-  ICING_ASSERT_OK(doc_store->Put(document0));
-  ICING_ASSERT_OK(doc_store->Put(document1));
-  ICING_ASSERT_OK(doc_store->Put(document2));
-  ICING_ASSERT_OK(doc_store->Put(document3));
-  ICING_ASSERT_OK(doc_store->Put(document4));
+  ICING_ASSERT_OK(doc_store->Put(
+      CreateDocumentWrapper(document0, /*num_string_tokens=*/0)));
+  ICING_ASSERT_OK(doc_store->Put(
+      CreateDocumentWrapper(document1, /*num_string_tokens=*/0)));
+  ICING_ASSERT_OK(doc_store->Put(
+      CreateDocumentWrapper(document2, /*num_string_tokens=*/0)));
+  ICING_ASSERT_OK(doc_store->Put(
+      CreateDocumentWrapper(document3, /*num_string_tokens=*/0)));
+  ICING_ASSERT_OK(doc_store->Put(
+      CreateDocumentWrapper(document4, /*num_string_tokens=*/0)));
 
   std::string original_document_log = absl_ports::StrCat(
       document_store_dir_, "/", DocumentLogCreator::GetDocumentLogFilename());
@@ -1529,17 +2134,17 @@
     std::unique_ptr<DocumentStore> doc_store =
         std::move(create_result.document_store);
 
-    ICING_ASSERT_OK_AND_ASSIGN(
-        DocumentStore::PutResult put_result1,
-        doc_store->Put(DocumentProto(test_document1_), /*num_tokens=*/4));
+    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                               doc_store->Put(CreateDocumentWrapper(
+                                   test_document1_, /*num_string_tokens=*/4)));
     EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_result1.was_replacement());
+    EXPECT_FALSE(put_result1.was_replacement);
     document_id1 = put_result1.new_document_id;
-    ICING_ASSERT_OK_AND_ASSIGN(
-        DocumentStore::PutResult put_result2,
-        doc_store->Put(DocumentProto(test_document2_), /*num_tokens=*/4));
+    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
+                               doc_store->Put(CreateDocumentWrapper(
+                                   test_document2_, /*num_string_tokens=*/4)));
     EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_result2.was_replacement());
+    EXPECT_FALSE(put_result2.was_replacement);
     document_id2 = put_result2.new_document_id;
     EXPECT_THAT(doc_store->Get(document_id1),
                 IsOkAndHolds(EqualsProto(test_document1_)));
@@ -1615,11 +2220,10 @@
       DocumentFilterData doc_filter_data,
       doc_store->GetAliveDocumentFilterData(
           document_id2, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(
-      doc_filter_data,
-      Eq(DocumentFilterData(
-          /*namespace_id=*/0, tc3farmhash::Fingerprint64(test_document2_.uri()),
-          /*schema_type_id=*/0, document2_expiration_timestamp_)));
+  EXPECT_THAT(doc_filter_data,
+              Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                    document2_expiration_timestamp_,
+                                    document2_expiration_timestamp_)));
 
   // Checks derived score cache
   EXPECT_THAT(
@@ -1655,17 +2259,17 @@
     std::unique_ptr<DocumentStore> doc_store =
         std::move(create_result.document_store);
 
-    ICING_ASSERT_OK_AND_ASSIGN(
-        DocumentStore::PutResult put_result1,
-        doc_store->Put(DocumentProto(test_document1_), /*num_tokens=*/4));
+    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                               doc_store->Put(CreateDocumentWrapper(
+                                   test_document1_, /*num_string_tokens=*/4)));
     EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_result1.was_replacement());
+    EXPECT_FALSE(put_result1.was_replacement);
     document_id1 = put_result1.new_document_id;
-    ICING_ASSERT_OK_AND_ASSIGN(
-        DocumentStore::PutResult put_result2,
-        doc_store->Put(DocumentProto(test_document2_), /*num_tokens=*/4));
+    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
+                               doc_store->Put(CreateDocumentWrapper(
+                                   test_document2_, /*num_string_tokens=*/4)));
     EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_result2.was_replacement());
+    EXPECT_FALSE(put_result2.was_replacement);
     document_id2 = put_result2.new_document_id;
     EXPECT_THAT(doc_store->Get(document_id1),
                 IsOkAndHolds(EqualsProto(test_document1_)));
@@ -1751,11 +2355,10 @@
       DocumentFilterData doc_filter_data,
       doc_store->GetAliveDocumentFilterData(
           document_id2, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(
-      doc_filter_data,
-      Eq(DocumentFilterData(
-          /*namespace_id=*/0, tc3farmhash::Fingerprint64(test_document2_.uri()),
-          /*schema_type_id=*/0, document2_expiration_timestamp_)));
+  EXPECT_THAT(doc_filter_data,
+              Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                    document2_expiration_timestamp_,
+                                    document2_expiration_timestamp_)));
 
   // Checks derived score cache
   EXPECT_THAT(
@@ -1801,17 +2404,17 @@
     std::unique_ptr<DocumentStore> doc_store =
         std::move(create_result.document_store);
 
-    ICING_ASSERT_OK_AND_ASSIGN(
-        DocumentStore::PutResult put_result1,
-        doc_store->Put(DocumentProto(test_document1_), /*num_tokens=*/4));
+    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                               doc_store->Put(CreateDocumentWrapper(
+                                   test_document1_, /*num_string_tokens=*/4)));
     EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_result1.was_replacement());
+    EXPECT_FALSE(put_result1.was_replacement);
     document_id1 = put_result1.new_document_id;
-    ICING_ASSERT_OK_AND_ASSIGN(
-        DocumentStore::PutResult put_result2,
-        doc_store->Put(DocumentProto(test_document2_), /*num_tokens=*/4));
+    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
+                               doc_store->Put(CreateDocumentWrapper(
+                                   test_document2_, /*num_string_tokens=*/4)));
     EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_result2.was_replacement());
+    EXPECT_FALSE(put_result2.was_replacement);
     document_id2 = put_result2.new_document_id;
     EXPECT_THAT(doc_store->Get(document_id1),
                 IsOkAndHolds(EqualsProto(test_document1_)));
@@ -1885,11 +2488,10 @@
       DocumentFilterData doc_filter_data,
       doc_store->GetAliveDocumentFilterData(
           document_id2, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(
-      doc_filter_data,
-      Eq(DocumentFilterData(
-          /*namespace_id=*/0, tc3farmhash::Fingerprint64(test_document2_.uri()),
-          /*schema_type_id=*/0, document2_expiration_timestamp_)));
+  EXPECT_THAT(doc_filter_data,
+              Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                    document2_expiration_timestamp_,
+                                    document2_expiration_timestamp_)));
 
   // Checks derived score cache.
   EXPECT_THAT(
@@ -1935,17 +2537,17 @@
     std::unique_ptr<DocumentStore> doc_store =
         std::move(create_result.document_store);
 
-    ICING_ASSERT_OK_AND_ASSIGN(
-        DocumentStore::PutResult put_result1,
-        doc_store->Put(DocumentProto(test_document1_), /*num_tokens=*/4));
+    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                               doc_store->Put(CreateDocumentWrapper(
+                                   test_document1_, /*num_string_tokens=*/4)));
     EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_result1.was_replacement());
+    EXPECT_FALSE(put_result1.was_replacement);
     document_id1 = put_result1.new_document_id;
-    ICING_ASSERT_OK_AND_ASSIGN(
-        DocumentStore::PutResult put_result2,
-        doc_store->Put(DocumentProto(test_document2_), /*num_tokens=*/4));
+    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
+                               doc_store->Put(CreateDocumentWrapper(
+                                   test_document2_, /*num_string_tokens=*/4)));
     EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_result2.was_replacement());
+    EXPECT_FALSE(put_result2.was_replacement);
     document_id2 = put_result2.new_document_id;
     EXPECT_THAT(doc_store->Get(document_id1),
                 IsOkAndHolds(EqualsProto(test_document1_)));
@@ -2010,11 +2612,10 @@
       DocumentFilterData doc_filter_data,
       doc_store->GetAliveDocumentFilterData(
           document_id2, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(
-      doc_filter_data,
-      Eq(DocumentFilterData(
-          /*namespace_id=*/0, tc3farmhash::Fingerprint64(test_document2_.uri()),
-          /*schema_type_id=*/0, document2_expiration_timestamp_)));
+  EXPECT_THAT(doc_filter_data,
+              Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                    document2_expiration_timestamp_,
+                                    document2_expiration_timestamp_)));
   // Checks derived score cache
   EXPECT_THAT(
       doc_store->GetDocumentAssociatedScoreData(document_id2),
@@ -2061,7 +2662,8 @@
   // 1 block size. The number 100 is a bit arbitrary, gotten from manually
   // testing.
   for (int i = 0; i < 100; ++i) {
-    ICING_ASSERT_OK(doc_store->Put(document));
+    ICING_ASSERT_OK(
+        doc_store->Put(document_util::CreateDocumentWrapper(document)));
   }
   doc_store_storage_info = doc_store->GetStorageInfo();
   EXPECT_THAT(doc_store_storage_info.document_store_size(),
@@ -2093,10 +2695,11 @@
   // Since the DocumentStore is empty, we get an invalid DocumentId
   EXPECT_THAT(doc_store->last_added_document_id(), Eq(kInvalidDocumentId));
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(DocumentProto(test_document1_)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(document_util::CreateDocumentWrapper(test_document1_)));
   EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result1.was_replacement());
+  EXPECT_FALSE(put_result1.was_replacement);
   DocumentId document_id1 = put_result1.new_document_id;
   EXPECT_THAT(doc_store->last_added_document_id(), Eq(document_id1));
 
@@ -2105,10 +2708,11 @@
                                     fake_clock_.GetSystemTimeMilliseconds()));
   EXPECT_THAT(doc_store->last_added_document_id(), Eq(document_id1));
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(DocumentProto(test_document2_)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(document_util::CreateDocumentWrapper(test_document2_)));
   EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result2.was_replacement());
+  EXPECT_FALSE(put_result2.was_replacement);
   DocumentId document_id2 = put_result2.new_document_id;
   EXPECT_THAT(doc_store->last_added_document_id(), Eq(document_id2));
 }
@@ -2126,8 +2730,10 @@
   DocumentProto document_namespace2 =
       DocumentBuilder().SetKey("namespace2", "2").SetSchema("email").Build();
 
-  ICING_ASSERT_OK(doc_store->Put(DocumentProto(document_namespace1)));
-  ICING_ASSERT_OK(doc_store->Put(DocumentProto(document_namespace2)));
+  ICING_ASSERT_OK(doc_store->Put(
+      document_util::CreateDocumentWrapper(document_namespace1)));
+  ICING_ASSERT_OK(doc_store->Put(
+      document_util::CreateDocumentWrapper(document_namespace2)));
 
   // NamespaceId of 0 since it was the first namespace seen by the DocumentStore
   EXPECT_THAT(doc_store->GetNamespaceId("namespace1"), IsOkAndHolds(Eq(0)));
@@ -2159,8 +2765,10 @@
   DocumentProto document2 =
       DocumentBuilder().SetKey("namespace", "2").SetSchema("email").Build();
 
-  ICING_ASSERT_OK(doc_store->Put(document1));
-  ICING_ASSERT_OK(doc_store->Put(document2));
+  ICING_ASSERT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(document1)));
+  ICING_ASSERT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(document2)));
 
   // NamespaceId of 0 since it was the first namespace seen by the DocumentStore
   EXPECT_THAT(doc_store->GetNamespaceId("namespace"), IsOkAndHolds(Eq(0)));
@@ -2191,8 +2799,10 @@
   DocumentProto document2 =
       DocumentBuilder().SetKey("namespace", "2").SetSchema("email").Build();
 
-  ICING_ASSERT_OK(doc_store->Put(document1));
-  ICING_ASSERT_OK(doc_store->Put(document2));
+  ICING_ASSERT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(document1)));
+  ICING_ASSERT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(document2)));
 
   // CorpusId of 0 since it was the first namespace seen by the DocumentStore
   EXPECT_THAT(doc_store->GetCorpusId("namespace", "email"),
@@ -2212,8 +2822,10 @@
   DocumentProto document_corpus2 =
       DocumentBuilder().SetKey("namespace2", "2").SetSchema("email").Build();
 
-  ICING_ASSERT_OK(doc_store->Put(DocumentProto(document_corpus1)));
-  ICING_ASSERT_OK(doc_store->Put(DocumentProto(document_corpus2)));
+  ICING_ASSERT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(document_corpus1)));
+  ICING_ASSERT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(document_corpus2)));
 
   // CorpusId of 0 since it was the first corpus seen by the DocumentStore
   EXPECT_THAT(doc_store->GetCorpusId("namespace1", "email"),
@@ -2248,7 +2860,8 @@
 
   DocumentProto document_corpus =
       DocumentBuilder().SetKey("namespace1", "1").SetSchema("email").Build();
-  ICING_ASSERT_OK(doc_store->Put(DocumentProto(document_corpus)));
+  ICING_ASSERT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(document_corpus)));
 
   EXPECT_THAT(doc_store->GetCorpusId("nonexistent_namespace", "email"),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
@@ -2271,8 +2884,10 @@
   DocumentProto document2 =
       DocumentBuilder().SetKey("namespace", "2").SetSchema("email").Build();
 
-  ICING_ASSERT_OK(doc_store->Put(document1, /*num_tokens=*/5));
-  ICING_ASSERT_OK(doc_store->Put(document2, /*num_tokens=*/7));
+  ICING_ASSERT_OK(doc_store->Put(
+      CreateDocumentWrapper(document1, /*num_string_tokens=*/5)));
+  ICING_ASSERT_OK(doc_store->Put(
+      CreateDocumentWrapper(document2, /*num_string_tokens=*/7)));
 
   // CorpusId of 0 since it was the first namespace seen by the DocumentStore
   EXPECT_THAT(doc_store->GetCorpusAssociatedScoreData(/*corpus_id=*/0),
@@ -2296,10 +2911,10 @@
   DocumentProto document_corpus2 =
       DocumentBuilder().SetKey("namespace2", "2").SetSchema("email").Build();
 
-  ICING_ASSERT_OK(
-      doc_store->Put(DocumentProto(document_corpus1), /*num_tokens=*/5));
-  ICING_ASSERT_OK(
-      doc_store->Put(DocumentProto(document_corpus2), /*num_tokens=*/7));
+  ICING_ASSERT_OK(doc_store->Put(
+      CreateDocumentWrapper(document_corpus1, /*num_string_tokens=*/5)));
+  ICING_ASSERT_OK(doc_store->Put(
+      CreateDocumentWrapper(document_corpus2, /*num_string_tokens=*/7)));
 
   // CorpusId of 0 since it was the first corpus seen by the DocumentStore
   EXPECT_THAT(doc_store->GetCorpusAssociatedScoreData(/*corpus_id=*/0),
@@ -2358,17 +2973,17 @@
               document2_creation_timestamp_)  // A random timestamp
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result1,
-      doc_store->Put(DocumentProto(document1), /*num_tokens=*/5));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                             doc_store->Put(CreateDocumentWrapper(
+                                 document1, /*num_string_tokens=*/5)));
   EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result1.was_replacement());
+  EXPECT_FALSE(put_result1.was_replacement);
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result2,
-      doc_store->Put(DocumentProto(document2), /*num_tokens=*/7));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
+                             doc_store->Put(CreateDocumentWrapper(
+                                 document2, /*num_string_tokens=*/7)));
   EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result2.was_replacement());
+  EXPECT_FALSE(put_result2.was_replacement);
   DocumentId document_id2 = put_result2.new_document_id;
 
   EXPECT_THAT(
@@ -2410,17 +3025,17 @@
               document2_creation_timestamp_)  // A random timestamp
           .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result1,
-      doc_store->Put(DocumentProto(document1), /*num_tokens=*/5));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                             doc_store->Put(CreateDocumentWrapper(
+                                 document1, /*num_string_tokens=*/5)));
   EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result1.was_replacement());
+  EXPECT_FALSE(put_result1.was_replacement);
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result2,
-      doc_store->Put(DocumentProto(document2), /*num_tokens=*/7));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
+                             doc_store->Put(CreateDocumentWrapper(
+                                 document2, /*num_string_tokens=*/7)));
   EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result2.was_replacement());
+  EXPECT_FALSE(put_result2.was_replacement);
   DocumentId document_id2 = put_result2.new_document_id;
 
   EXPECT_THAT(
@@ -2469,21 +3084,21 @@
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store->Put(DocumentProto(test_document1_)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(test_document1_)));
   EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result.was_replacement());
+  EXPECT_FALSE(put_result.was_replacement);
   DocumentId document_id = put_result.new_document_id;
 
   ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
       DocumentFilterData doc_filter_data,
       doc_store->GetAliveDocumentFilterData(
           document_id, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(
-      doc_filter_data,
-      Eq(DocumentFilterData(
-          /*namespace_id=*/0, tc3farmhash::Fingerprint64(test_document1_.uri()),
-          /*schema_type_id=*/0, document1_expiration_timestamp_)));
+  EXPECT_THAT(doc_filter_data,
+              Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                    document1_expiration_timestamp_,
+                                    document1_expiration_timestamp_)));
 
   ICING_ASSERT_OK(doc_store->Delete("icing", "email/1",
                                     fake_clock_.GetSystemTimeMilliseconds()));
@@ -2500,11 +3115,11 @@
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(
-      DocumentStore::PutResult put_result,
-      doc_store->Put(DocumentProto(test_document1_), /*num_tokens=*/4));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             doc_store->Put(CreateDocumentWrapper(
+                                 test_document1_, /*num_string_tokens=*/4)));
   EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result.was_replacement());
+  EXPECT_FALSE(put_result.was_replacement);
   DocumentId document_id = put_result.new_document_id;
 
   EXPECT_THAT(doc_store->GetDocumentAssociatedScoreData(document_id),
@@ -2536,7 +3151,8 @@
       std::move(create_result.document_store);
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(test_document1_, /*num_tokens=*/4));
+                             doc_store->Put(CreateDocumentWrapper(
+                                 test_document1_, /*num_string_tokens=*/4)));
   DocumentId document_id = put_result1.new_document_id;
   EXPECT_NE(doc_store->GetScorablePropertySet(
                 document_id, fake_clock_.GetSystemTimeMilliseconds()),
@@ -2557,10 +3173,11 @@
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store->Put(DocumentProto(test_document1_)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(test_document1_)));
   EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result.was_replacement());
+  EXPECT_FALSE(put_result.was_replacement);
   DocumentId document_id = put_result.new_document_id;
 
   // Report usage with type 1.
@@ -2609,10 +3226,11 @@
                                .SetTtlMs(100)
                                .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document)));
   EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result.was_replacement());
+  EXPECT_FALSE(put_result.was_replacement);
   DocumentId document_id = put_result.new_document_id;
 
   // Some arbitrary time before the document's creation time (10) + ttl (100)
@@ -2645,6 +3263,252 @@
       document_id, fake_clock_.GetSystemTimeMilliseconds()));
 }
 
+TEST_P(DocumentStoreTest, IsDocumentAlive) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace", "uri")
+                               .SetSchema("email")
+                               .SetCreationTimestampMs(0)
+                               .SetTtlMs(1000)
+                               .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document)));
+  ASSERT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  ASSERT_FALSE(put_result.was_replacement);
+  DocumentId document_id = put_result.new_document_id;
+
+  // The expiration timestamp is 1000, so the document is alive at any time
+  // before that.
+  EXPECT_TRUE(doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/0));
+  EXPECT_TRUE(doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/100));
+  EXPECT_TRUE(doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/999));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/1000));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/1001));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/2000));
+
+  EXPECT_TRUE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/0));
+  EXPECT_TRUE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/100));
+  EXPECT_TRUE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/999));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/1000));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/1001));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/2000));
+}
+
+TEST_P(DocumentStoreTest, IsDocumentAlive_deletedDocument) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace", "uri")
+                               .SetSchema("email")
+                               .SetCreationTimestampMs(0)
+                               .SetTtlMs(1000)
+                               .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document)));
+  ASSERT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  ASSERT_FALSE(put_result.was_replacement);
+  DocumentId document_id = put_result.new_document_id;
+
+  // Delete the document.
+  ICING_ASSERT_OK(doc_store->Delete("namespace", "uri",
+                                    fake_clock_.GetSystemTimeMilliseconds()));
+
+  // Should be non-alive regardless of the current timestamp.
+  EXPECT_FALSE(doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/0));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/100));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/1099));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/1100));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/1101));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/2000));
+
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/0));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/100));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/1099));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/1100));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/1101));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/2000));
+}
+
+TEST_P(DocumentStoreTest, IsDocumentAlive_infiniteTtlDocument) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace", "uri")
+                               .SetSchema("email")
+                               .SetCreationTimestampMs(0)
+                               .SetTtlMs(0)  // infinite TTL
+                               .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document)));
+  ASSERT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  ASSERT_FALSE(put_result.was_replacement);
+  DocumentId document_id = put_result.new_document_id;
+
+  EXPECT_TRUE(doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/0));
+  EXPECT_TRUE(doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/100));
+  EXPECT_TRUE(
+      doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/1099));
+  EXPECT_TRUE(
+      doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/1100));
+  EXPECT_TRUE(
+      doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/1101));
+  EXPECT_TRUE(
+      doc_store->IsDocumentAlive(document_id, /*current_time_ms=*/2000));
+  EXPECT_TRUE(doc_store->IsDocumentAlive(
+      document_id,
+      /*current_time_ms=*/std::numeric_limits<int64_t>::max() - 1));
+
+  EXPECT_TRUE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/0));
+  EXPECT_TRUE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/100));
+  EXPECT_TRUE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/1099));
+  EXPECT_TRUE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/1100));
+  EXPECT_TRUE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/1101));
+  EXPECT_TRUE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/2000));
+  EXPECT_TRUE(doc_store->IsDocumentAlive(
+      "namespace", "uri",
+      /*current_time_ms=*/std::numeric_limits<int64_t>::max() - 1));
+}
+
+TEST_P(DocumentStoreTest, IsDocumentAlive_invalidDocumentId) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  ASSERT_THAT(doc_store->last_added_document_id(), Eq(kInvalidDocumentId));
+
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive(/*document_id=*/-1, /*current_time_ms=*/0));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive(/*document_id=*/0, /*current_time_ms=*/0));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive(/*document_id=*/10000, /*current_time_ms=*/0));
+  EXPECT_FALSE(doc_store->IsDocumentAlive(/*document_id=*/100000,
+                                          /*current_time_ms=*/0));
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive(kInvalidDocumentId, /*current_time_ms=*/0));
+}
+
+TEST_P(DocumentStoreTest, IsDocumentAlive_invalidNamespaceUri) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  ASSERT_THAT(doc_store->last_added_document_id(), Eq(kInvalidDocumentId));
+
+  EXPECT_FALSE(
+      doc_store->IsDocumentAlive("namespace", "uri", /*current_time_ms=*/0));
+}
+
+TEST_P(DocumentStoreTest,
+       GetAliveDocumentFilterDataShouldCheckExpirationTimestamp) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace1", "1")
+                               .SetSchema("email")
+                               .SetCreationTimestampMs(100)
+                               .SetTtlMs(1000)
+                               .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document)));
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result.was_replacement);
+  DocumentId document_id = put_result.new_document_id;
+
+  // The raw expiration timestamp is 1100. Now, set the expiration timestamp to
+  // 500.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateDocumentExpirationTimestampResult update_result,
+      doc_store->UpdateDocumentExpirationTimestamp(
+          document_id, /*expiration_timestamp_ms=*/500));
+  EXPECT_THAT(update_result.final_expiration_timestamp_ms, Eq(500));
+  EXPECT_THAT(update_result.was_updated, IsTrue());
+
+  // Case 1: call GetAliveDocumentFilterData with a current ts before the
+  //         expiration timestamp.
+  EXPECT_THAT(
+      doc_store->GetAliveDocumentFilterData(document_id,
+                                            /*current_time_ms=*/300),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/500,
+                                     /*raw_expiration_timestamp_ms=*/1100))));
+
+  // Case 2: call GetAliveDocumentFilterData with a current ts at the
+  //         expiration timestamp.
+  // This should fail even though the current ts is smaller than the raw
+  // expiration timestamp.
+  EXPECT_FALSE(doc_store->GetAliveDocumentFilterData(document_id,
+                                                     /*current_time_ms=*/500));
+
+  // Case 3: call GetAliveDocumentFilterData with a current ts after the
+  //         expiration timestamp.
+  // This should fail even though the current ts is smaller than the raw
+  // expiration timestamp.
+  EXPECT_FALSE(doc_store->GetAliveDocumentFilterData(document_id,
+                                                     /*current_time_ms=*/700));
+}
+
 TEST_P(DocumentStoreTest,
        ExpirationTimestampIsSumOfNonZeroTtlAndCreationTimestamp) {
   DocumentProto document = DocumentBuilder()
@@ -2661,20 +3525,20 @@
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document)));
   EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result.was_replacement());
+  EXPECT_FALSE(put_result.was_replacement);
   DocumentId document_id = put_result.new_document_id;
   ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
       DocumentFilterData doc_filter_data,
       doc_store->GetAliveDocumentFilterData(
           document_id, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(
-      doc_filter_data,
-      Eq(DocumentFilterData(
-          /*namespace_id=*/0, tc3farmhash::Fingerprint64(document.uri()),
-          /*schema_type_id=*/0, /*expiration_timestamp_ms=*/1100)));
+  EXPECT_THAT(doc_filter_data, Eq(DocumentFilterData(
+                                   /*namespace_id=*/0, /*schema_type_id=*/0,
+                                   /*expiration_timestamp_ms=*/1100,
+                                   /*raw_expiration_timestamp_ms=*/1100)));
 }
 
 TEST_P(DocumentStoreTest, ExpirationTimestampIsInt64MaxIfTtlIsZero) {
@@ -2692,10 +3556,11 @@
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document)));
   EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result.was_replacement());
+  EXPECT_FALSE(put_result.was_replacement);
   DocumentId document_id = put_result.new_document_id;
 
   ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
@@ -2706,9 +3571,10 @@
   EXPECT_THAT(
       doc_filter_data,
       Eq(DocumentFilterData(
-          /*namespace_id=*/0, tc3farmhash::Fingerprint64(document.uri()),
-          /*schema_type_id=*/0,
-          /*expiration_timestamp_ms=*/std::numeric_limits<int64_t>::max())));
+          /*namespace_id=*/0, /*schema_type_id=*/0,
+          /*expiration_timestamp_ms=*/std::numeric_limits<int64_t>::max(),
+          /*raw_expiration_timestamp_ms=*/
+          std::numeric_limits<int64_t>::max())));
 }
 
 TEST_P(DocumentStoreTest, ExpirationTimestampIsInt64MaxOnOverflow) {
@@ -2727,10 +3593,11 @@
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document)));
   EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result.was_replacement());
+  EXPECT_FALSE(put_result.was_replacement);
   DocumentId document_id = put_result.new_document_id;
 
   ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
@@ -2741,23 +3608,20 @@
   EXPECT_THAT(
       doc_filter_data,
       Eq(DocumentFilterData(
-          /*namespace_id=*/0, tc3farmhash::Fingerprint64(document.uri()),
-          /*schema_type_id=*/0,
-          /*expiration_timestamp_ms=*/std::numeric_limits<int64_t>::max())));
+          /*namespace_id=*/0, /*schema_type_id=*/0,
+          /*expiration_timestamp_ms=*/std::numeric_limits<int64_t>::max(),
+          /*raw_expiration_timestamp_ms=*/
+          std::numeric_limits<int64_t>::max())));
 }
 
-TEST_P(DocumentStoreTest, CreationTimestampShouldBePopulated) {
-  // Creates a document without a given creation timestamp
-  DocumentProto document_without_creation_timestamp =
-      DocumentBuilder()
-          .SetKey("icing", "email/1")
-          .SetSchema("email")
-          .AddStringProperty("subject", "subject foo")
-          .AddStringProperty("body", "body bar")
-          .Build();
+TEST_P(DocumentStoreTest, UpdateDocumentExpirationTimestamp) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace1", "1")
+                               .SetSchema("email")
+                               .SetCreationTimestampMs(100)
+                               .SetTtlMs(1000)
+                               .Build();
 
-  int64_t fake_real_time = 100;
-  fake_clock_.SetSystemTimeMilliseconds(fake_real_time);
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
@@ -2767,17 +3631,475 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
-      doc_store->Put(document_without_creation_timestamp));
+      doc_store->Put(document_util::CreateDocumentWrapper(document)));
   EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result.was_replacement());
+  EXPECT_FALSE(put_result.was_replacement);
   DocumentId document_id = put_result.new_document_id;
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentProto document_with_creation_timestamp,
-                             doc_store->Get(document_id));
+  EXPECT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/1100,
+                                     /*raw_expiration_timestamp_ms=*/1100))));
 
-  // Now the creation timestamp should be set by document store.
-  EXPECT_THAT(document_with_creation_timestamp.creation_timestamp_ms(),
-              Eq(fake_real_time));
+  // Set the expiration timestamp to 700.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateDocumentExpirationTimestampResult update_result1,
+      doc_store->UpdateDocumentExpirationTimestamp(
+          document_id, /*expiration_timestamp_ms=*/700));
+  EXPECT_THAT(update_result1.final_expiration_timestamp_ms, Eq(700));
+  EXPECT_THAT(update_result1.was_updated, IsTrue());
+  EXPECT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/700,
+                                     /*raw_expiration_timestamp_ms=*/1100))));
+
+  // Set the expiration timestamp to 500.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateDocumentExpirationTimestampResult update_result2,
+      doc_store->UpdateDocumentExpirationTimestamp(
+          document_id, /*expiration_timestamp_ms=*/500));
+  EXPECT_THAT(update_result2.final_expiration_timestamp_ms, Eq(500));
+  EXPECT_THAT(update_result2.was_updated, IsTrue());
+  EXPECT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/500,
+                                     /*raw_expiration_timestamp_ms=*/1100))));
+}
+
+TEST_P(DocumentStoreTest, UpdateDocumentExpirationTimestamp_invalidDocumentId) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace1", "1")
+                               .SetSchema("email")
+                               .SetCreationTimestampMs(100)
+                               .SetTtlMs(1000)
+                               .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document)));
+  ASSERT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  ASSERT_FALSE(put_result.was_replacement);
+
+  EXPECT_THAT(doc_store->UpdateDocumentExpirationTimestamp(
+                  /*document_id=*/-1, /*expiration_timestamp_ms=*/700),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(doc_store->UpdateDocumentExpirationTimestamp(
+                  doc_store->last_added_document_id() + 1,
+                  /*expiration_timestamp_ms=*/700),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(doc_store->UpdateDocumentExpirationTimestamp(
+                  kInvalidDocumentId, /*expiration_timestamp_ms=*/700),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+TEST_P(DocumentStoreTest,
+       UpdateDocumentExpirationTimestamp_shouldOnlyDecrease) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace1", "1")
+                               .SetSchema("email")
+                               .SetCreationTimestampMs(100)
+                               .SetTtlMs(1000)
+                               .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document)));
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result.was_replacement);
+  DocumentId document_id = put_result.new_document_id;
+
+  // The raw expiration timestamp and the expiration timestamp are 1100.
+  // Setting the expiration timestamp to 1500 should be no-op.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateDocumentExpirationTimestampResult update_result1,
+      doc_store->UpdateDocumentExpirationTimestamp(
+          document_id, /*expiration_timestamp_ms=*/1500));
+  EXPECT_THAT(update_result1.final_expiration_timestamp_ms, Eq(1100));
+  EXPECT_THAT(update_result1.was_updated, IsFalse());
+  EXPECT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/1100,
+                                     /*raw_expiration_timestamp_ms=*/1100))));
+
+  // Set the expiration timestamp to 500. This should change the expiration
+  // timestamp to 500.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateDocumentExpirationTimestampResult update_result2,
+      doc_store->UpdateDocumentExpirationTimestamp(
+          document_id, /*expiration_timestamp_ms=*/500));
+  EXPECT_THAT(update_result2.final_expiration_timestamp_ms, Eq(500));
+  EXPECT_THAT(update_result2.was_updated, IsTrue());
+  EXPECT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/500,
+                                     /*raw_expiration_timestamp_ms=*/1100))));
+
+  // Setting the expiration timestamp to 700 should be no-op.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateDocumentExpirationTimestampResult update_result3,
+      doc_store->UpdateDocumentExpirationTimestamp(
+          document_id, /*expiration_timestamp_ms=*/700));
+  EXPECT_THAT(update_result3.final_expiration_timestamp_ms, Eq(500));
+  EXPECT_THAT(update_result3.was_updated, IsFalse());
+  EXPECT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/500,
+                                     /*raw_expiration_timestamp_ms=*/1100))));
+}
+
+TEST_P(DocumentStoreTest,
+       UpdateDocumentExpirationTimestamp_canUpdateExpiredDocument) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace1", "1")
+                               .SetSchema("email")
+                               .SetCreationTimestampMs(100)
+                               .SetTtlMs(1000)
+                               .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document)));
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result.was_replacement);
+  DocumentId document_id = put_result.new_document_id;
+
+  // Set the expiration timestamp to 500.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateDocumentExpirationTimestampResult update_result1,
+      doc_store->UpdateDocumentExpirationTimestamp(
+          document_id, /*expiration_timestamp_ms=*/500));
+  ASSERT_THAT(update_result1.final_expiration_timestamp_ms, Eq(500));
+  ASSERT_THAT(update_result1.was_updated, IsTrue());
+  // Sanity check.
+  ASSERT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/500,
+                                     /*raw_expiration_timestamp_ms=*/1100))));
+
+  // Adjust the current time to 500. The document is expired.
+  fake_clock_.SetSystemTimeMilliseconds(500);
+  ASSERT_THAT(doc_store->IsDocumentAlive(
+                  /*document_id=*/0,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsFalse());
+
+  // Updating an expired document is allowed, but since we only allow to
+  // decrease the expiration timestamp, it is a no-op.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateDocumentExpirationTimestampResult update_result2,
+      doc_store->UpdateDocumentExpirationTimestamp(
+          document_id, /*expiration_timestamp_ms=*/800));
+  EXPECT_THAT(update_result2.final_expiration_timestamp_ms, Eq(500));
+  EXPECT_THAT(update_result2.was_updated, IsFalse());
+  EXPECT_THAT(
+      doc_store->GetNonDeletedDocumentFilterData(document_id),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/500,
+                                     /*raw_expiration_timestamp_ms=*/1100))));
+
+  // Updating an expired document is allowed, and it only takes effect if the
+  // new expiration timestamp is smaller than the current one, so the document
+  // remains expired.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateDocumentExpirationTimestampResult update_result3,
+      doc_store->UpdateDocumentExpirationTimestamp(
+          document_id, /*expiration_timestamp_ms=*/400));
+  EXPECT_THAT(update_result3.final_expiration_timestamp_ms, Eq(400));
+  EXPECT_THAT(update_result3.was_updated, IsTrue());
+  EXPECT_THAT(
+      doc_store->GetNonDeletedDocumentFilterData(document_id),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/400,
+                                     /*raw_expiration_timestamp_ms=*/1100))));
+}
+
+TEST_P(DocumentStoreTest,
+       UpdateDocumentExpirationTimestamp_shouldFailForDeletedDocument) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace1", "1")
+                               .SetSchema("email")
+                               .SetCreationTimestampMs(100)
+                               .SetTtlMs(1000)
+                               .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document)));
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result.was_replacement);
+  DocumentId document_id = put_result.new_document_id;
+
+  // Delete the document.
+  ICING_ASSERT_OK(doc_store->Delete(
+      document_id,
+      /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+
+  EXPECT_THAT(doc_store->UpdateDocumentExpirationTimestamp(
+                  document_id, /*expiration_timestamp_ms=*/500),
+              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
+}
+
+TEST_P(DocumentStoreTest,
+       UpdateDocumentExpirationTimestamp_canUpdateToAnExpiredTimestamp) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace1", "1")
+                               .SetSchema("email")
+                               .SetCreationTimestampMs(100)
+                               .SetTtlMs(1000)
+                               .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document)));
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result.was_replacement);
+  DocumentId document_id = put_result.new_document_id;
+
+  // Adjust the current time to 500. The document is still alive.
+  fake_clock_.SetSystemTimeMilliseconds(500);
+  ASSERT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/1100,
+                                     /*raw_expiration_timestamp_ms=*/1100))));
+
+  // Set the expiration timestamp to 400. It is allowed to set the expiration
+  // timestamp to a smaller value than the current timestamp, and the document
+  // will be treated as expired.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateDocumentExpirationTimestampResult update_result,
+      doc_store->UpdateDocumentExpirationTimestamp(
+          document_id, /*expiration_timestamp_ms=*/400));
+  EXPECT_THAT(update_result.final_expiration_timestamp_ms, Eq(400));
+  EXPECT_THAT(update_result.was_updated, IsTrue());
+  EXPECT_THAT(doc_store->IsDocumentAlive(
+                  document_id,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsFalse());
+}
+
+TEST_P(DocumentStoreTest, ResetAllAliveExpirationTimestampsToRaw) {
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("namespace1", "1")
+                                .SetSchema("email")
+                                .SetCreationTimestampMs(100)
+                                .SetTtlMs(1000)
+                                .Build();
+  DocumentProto document2 =
+      DocumentBuilder()
+          .SetKey("namespace1", "2")
+          .SetSchema("email")
+          .SetCreationTimestampMs(300)
+          .SetTtlMs(0)  // No TTL. The expiration timestamp will be INT64_MAX.
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(document_util::CreateDocumentWrapper(document1)));
+  EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result1.was_replacement);
+  DocumentId document_id1 = put_result1.new_document_id;
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(document_util::CreateDocumentWrapper(document2)));
+  EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result2.was_replacement);
+  DocumentId document_id2 = put_result2.new_document_id;
+
+  // Update the expiration timestamp of document1 and document2.
+  ICING_ASSERT_OK(doc_store->UpdateDocumentExpirationTimestamp(
+      document_id1, /*expiration_timestamp_ms=*/500));
+  ICING_ASSERT_OK(doc_store->UpdateDocumentExpirationTimestamp(
+      document_id2, /*expiration_timestamp_ms=*/3000));
+  // Sanity check.
+  ASSERT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id1, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/500,
+                                     /*raw_expiration_timestamp_ms=*/1100))));
+  ASSERT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id2, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/3000,
+                                     /*raw_expiration_timestamp_ms=*/
+                                     std::numeric_limits<int64_t>::max()))));
+
+  // Reset.
+  EXPECT_THAT(doc_store->ResetAllAliveExpirationTimestampsToRaw(
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()),
+              IsOk());
+  EXPECT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id1, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/1100,
+                                     /*raw_expiration_timestamp_ms=*/1100))));
+  EXPECT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id2, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(
+          /*namespace_id=*/0, /*schema_type_id=*/0,
+          /*expiration_timestamp_ms=*/std::numeric_limits<int64_t>::max(),
+          /*raw_expiration_timestamp_ms=*/
+          std::numeric_limits<int64_t>::max()))));
+}
+
+TEST_P(DocumentStoreTest,
+       ResetAllAliveExpirationTimestampsToRaw_shouldSkipExpiredDocuments) {
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("namespace1", "1")
+                                .SetSchema("email")
+                                .SetCreationTimestampMs(100)
+                                .SetTtlMs(1000)
+                                .Build();
+  DocumentProto document2 =
+      DocumentBuilder()
+          .SetKey("namespace1", "2")
+          .SetSchema("email")
+          .SetCreationTimestampMs(300)
+          .SetTtlMs(0)  // No TTL. The expiration timestamp will be INT64_MAX.
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(document_util::CreateDocumentWrapper(document1)));
+  EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result1.was_replacement);
+  DocumentId document_id1 = put_result1.new_document_id;
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(document_util::CreateDocumentWrapper(document2)));
+  EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result2.was_replacement);
+  DocumentId document_id2 = put_result2.new_document_id;
+
+  // Update the expiration timestamp of document1 and document2.
+  ICING_ASSERT_OK(doc_store->UpdateDocumentExpirationTimestamp(
+      document_id1, /*expiration_timestamp_ms=*/500));
+  ICING_ASSERT_OK(doc_store->UpdateDocumentExpirationTimestamp(
+      document_id2, /*expiration_timestamp_ms=*/3000));
+  // Sanity check.
+  ASSERT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id1, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/500,
+                                     /*raw_expiration_timestamp_ms=*/1100))));
+  ASSERT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id2, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/3000,
+                                     /*raw_expiration_timestamp_ms=*/
+                                     std::numeric_limits<int64_t>::max()))));
+
+  // Reset with current timestamp 500. Document1 should be skipped since it is
+  // expired (at t = 500).
+  EXPECT_THAT(doc_store->ResetAllAliveExpirationTimestampsToRaw(
+                  /*current_time_ms=*/500),
+              IsOk());
+  EXPECT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id1, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/500,
+                                     /*raw_expiration_timestamp_ms=*/1100))));
+  EXPECT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id2, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(
+          /*namespace_id=*/0, /*schema_type_id=*/0,
+          /*expiration_timestamp_ms=*/std::numeric_limits<int64_t>::max(),
+          /*raw_expiration_timestamp_ms=*/
+          std::numeric_limits<int64_t>::max()))));
+
+  // Reset with current timestamp 700. Document1 should be skipped since it is
+  // expired (at t = 500).
+  EXPECT_THAT(doc_store->ResetAllAliveExpirationTimestampsToRaw(
+                  /*current_time_ms=*/700),
+              IsOk());
+  EXPECT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id1, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(/*namespace_id=*/0, /*schema_type_id=*/0,
+                                     /*expiration_timestamp_ms=*/500,
+                                     /*raw_expiration_timestamp_ms=*/1100))));
+  EXPECT_THAT(
+      doc_store->GetAliveDocumentFilterData(
+          document_id2, fake_clock_.GetSystemTimeMilliseconds()),
+      Optional(Eq(DocumentFilterData(
+          /*namespace_id=*/0, /*schema_type_id=*/0,
+          /*expiration_timestamp_ms=*/std::numeric_limits<int64_t>::max(),
+          /*raw_expiration_timestamp_ms=*/
+          std::numeric_limits<int64_t>::max()))));
 }
 
 TEST_P(DocumentStoreTest, ShouldWriteAndReadScoresCorrectly) {
@@ -2801,15 +4123,17 @@
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(document_util::CreateDocumentWrapper(document1)));
   EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result1.was_replacement());
+  EXPECT_FALSE(put_result1.was_replacement);
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(document_util::CreateDocumentWrapper(document2)));
   EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result2.was_replacement());
+  EXPECT_FALSE(put_result2.was_replacement);
   DocumentId document_id2 = put_result2.new_document_id;
 
   EXPECT_THAT(doc_store->GetDocumentAssociatedScoreData(document_id1),
@@ -2835,7 +4159,8 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_EXPECT_OK(document_store->Put(test_document1_));
+  ICING_EXPECT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(test_document1_)));
   // GetChecksum should succeed without updating the checksum.
   ICING_EXPECT_OK(document_store->GetChecksum());
 
@@ -2854,7 +4179,8 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_EXPECT_OK(document_store->Put(test_document1_));
+  ICING_EXPECT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(test_document1_)));
   ICING_ASSERT_OK_AND_ASSIGN(Crc32 checksum, document_store->GetChecksum());
   EXPECT_THAT(document_store->UpdateChecksum(), IsOkAndHolds(checksum));
   EXPECT_THAT(document_store->GetChecksum(), IsOkAndHolds(checksum));
@@ -2880,7 +4206,8 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_EXPECT_OK(document_store->Put(test_document1_));
+  ICING_EXPECT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(test_document1_)));
   // GetChecksum should return the same value as UpdateChecksum
   ICING_ASSERT_OK_AND_ASSIGN(Crc32 checksum, document_store->GetChecksum());
   EXPECT_THAT(document_store->UpdateChecksum(), IsOkAndHolds(checksum));
@@ -2898,12 +4225,14 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_EXPECT_OK(document_store->Put(test_document1_));
+  ICING_EXPECT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(test_document1_)));
   ICING_ASSERT_OK_AND_ASSIGN(Crc32 checksum, document_store->GetChecksum());
   EXPECT_THAT(document_store->UpdateChecksum(), IsOkAndHolds(checksum));
   EXPECT_THAT(document_store->GetChecksum(), IsOkAndHolds(checksum));
 
-  ICING_EXPECT_OK(document_store->Put(test_document2_));
+  ICING_EXPECT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(test_document2_)));
   EXPECT_THAT(document_store->GetChecksum(), IsOkAndHolds(Not(Eq(checksum))));
   EXPECT_THAT(document_store->UpdateChecksum(),
               IsOkAndHolds(Not(Eq(checksum))));
@@ -2917,7 +4246,8 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_EXPECT_OK(document_store->Put(test_document1_));
+  ICING_EXPECT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(test_document1_)));
   ICING_ASSERT_OK_AND_ASSIGN(Crc32 checksum, document_store->GetChecksum());
   EXPECT_THAT(document_store->UpdateChecksum(), IsOkAndHolds(checksum));
   EXPECT_THAT(document_store->GetChecksum(), IsOkAndHolds(checksum));
@@ -2983,9 +4313,10 @@
     // Insert and verify a "email "document
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::PutResult email_put_result,
-        document_store->Put(DocumentProto(email_document)));
+        document_store->Put(
+            document_util::CreateDocumentWrapper(email_document)));
     EXPECT_THAT(email_put_result.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(email_put_result.was_replacement());
+    EXPECT_FALSE(email_put_result.was_replacement);
     email_document_id = email_put_result.new_document_id;
     EXPECT_THAT(document_store->Get(email_document_id),
                 IsOkAndHolds(EqualsProto(email_document)));
@@ -2993,8 +4324,6 @@
         DocumentFilterData email_data,
         document_store->GetAliveDocumentFilterData(
             email_document_id, fake_clock_.GetSystemTimeMilliseconds()));
-    EXPECT_THAT(email_data.uri_fingerprint(),
-                Eq(tc3farmhash::Fingerprint64(email_document.uri())));
     EXPECT_THAT(email_data.schema_type_id(), Eq(email_schema_type_id));
     email_namespace_id = email_data.namespace_id();
     email_expiration_timestamp = email_data.expiration_timestamp_ms();
@@ -3002,9 +4331,10 @@
     // Insert and verify a "message" document
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::PutResult message_put_result,
-        document_store->Put(DocumentProto(message_document)));
+        document_store->Put(
+            document_util::CreateDocumentWrapper(message_document)));
     EXPECT_THAT(message_put_result.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(message_put_result.was_replacement());
+    EXPECT_FALSE(message_put_result.was_replacement);
     message_document_id = message_put_result.new_document_id;
     EXPECT_THAT(document_store->Get(message_document_id),
                 IsOkAndHolds(EqualsProto(message_document)));
@@ -3012,8 +4342,6 @@
         DocumentFilterData message_data,
         document_store->GetAliveDocumentFilterData(
             message_document_id, fake_clock_.GetSystemTimeMilliseconds()));
-    EXPECT_THAT(message_data.uri_fingerprint(),
-                Eq(tc3farmhash::Fingerprint64(message_document.uri())));
     EXPECT_THAT(message_data.schema_type_id(), Eq(message_schema_type_id));
     message_namespace_id = message_data.namespace_id();
     message_expiration_timestamp = message_data.expiration_timestamp_ms();
@@ -3058,12 +4386,12 @@
       document_store->GetAliveDocumentFilterData(
           email_document_id, fake_clock_.GetSystemTimeMilliseconds()));
   EXPECT_THAT(email_data.schema_type_id(), Eq(email_schema_type_id));
-  // Make sure that all the other fields are stll valid/the same
+  // Make sure that all the other fields are still valid/the same
   EXPECT_THAT(email_data.namespace_id(), Eq(email_namespace_id));
-  EXPECT_THAT(email_data.uri_fingerprint(),
-              Eq(tc3farmhash::Fingerprint64(email_document.uri())));
   EXPECT_THAT(email_data.expiration_timestamp_ms(),
               Eq(email_expiration_timestamp));
+  EXPECT_THAT(email_data.raw_expiration_timestamp_ms(),
+              Eq(email_expiration_timestamp));
 
   // "message" document has an invalid SchemaTypeId
   EXPECT_THAT(document_store->Get(message_document_id),
@@ -3073,12 +4401,12 @@
       document_store->GetAliveDocumentFilterData(
           message_document_id, fake_clock_.GetSystemTimeMilliseconds()));
   EXPECT_THAT(message_data.schema_type_id(), Eq(-1));
-  // Make sure that all the other fields are stll valid/the same
+  // Make sure that all the other fields are still valid/the same
   EXPECT_THAT(message_data.namespace_id(), Eq(message_namespace_id));
-  EXPECT_THAT(message_data.uri_fingerprint(),
-              Eq(tc3farmhash::Fingerprint64(message_document.uri())));
   EXPECT_THAT(message_data.expiration_timestamp_ms(),
               Eq(message_expiration_timestamp));
+  EXPECT_THAT(message_data.raw_expiration_timestamp_ms(),
+              Eq(message_expiration_timestamp));
 }
 
 TEST_P(DocumentStoreTest, UpdateSchemaStoreUpdatesSchemaTypeIds) {
@@ -3089,6 +4417,7 @@
   // Set a schema
   SchemaProto schema =
       SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("dummy"))
           .AddType(SchemaTypeConfigBuilder().SetType("email"))
           .AddType(SchemaTypeConfigBuilder().SetType("message"))
           .Build();
@@ -3127,69 +4456,69 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_put_result,
-      document_store->Put(DocumentProto(email_document)));
+      document_store->Put(
+          document_util::CreateDocumentWrapper(email_document)));
   EXPECT_THAT(email_put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(email_put_result.was_replacement());
+  EXPECT_FALSE(email_put_result.was_replacement);
   DocumentId email_document_id = email_put_result.new_document_id;
   ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
       DocumentFilterData email_data,
       document_store->GetAliveDocumentFilterData(
           email_document_id, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(email_data.uri_fingerprint(),
-              Eq(tc3farmhash::Fingerprint64(email_document.uri())));
   EXPECT_THAT(email_data.schema_type_id(), Eq(old_email_schema_type_id));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult message_put_result,
-      document_store->Put(DocumentProto(message_document)));
+      document_store->Put(
+          document_util::CreateDocumentWrapper(message_document)));
   EXPECT_THAT(message_put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(message_put_result.was_replacement());
+  EXPECT_FALSE(message_put_result.was_replacement);
   DocumentId message_document_id = message_put_result.new_document_id;
   ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
       DocumentFilterData message_data,
       document_store->GetAliveDocumentFilterData(
           message_document_id, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(message_data.uri_fingerprint(),
-              Eq(tc3farmhash::Fingerprint64(message_document.uri())));
   EXPECT_THAT(message_data.schema_type_id(), Eq(old_message_schema_type_id));
 
-  // Add a new schema type. Since SchemaTypeId is assigned based on order,
-  // this should change the SchemaTypeIds.
+  // Delete the first type. With schema type id optimization, this should change
+  // the type id of "message" but not "email."
   schema = SchemaBuilder()
-               .AddType(SchemaTypeConfigBuilder().SetType("newType"))
                .AddType(SchemaTypeConfigBuilder().SetType("email"))
                .AddType(SchemaTypeConfigBuilder().SetType("message"))
                .Build();
 
   ICING_EXPECT_OK(schema_store->SetSchema(
-      schema, /*ignore_errors_and_delete_documents=*/false));
+      schema, /*ignore_errors_and_delete_documents=*/true));
 
   ICING_ASSERT_OK_AND_ASSIGN(SchemaTypeId new_email_schema_type_id,
                              schema_store->GetSchemaTypeId("email"));
   ICING_ASSERT_OK_AND_ASSIGN(SchemaTypeId new_message_schema_type_id,
                              schema_store->GetSchemaTypeId("message"));
 
-  // SchemaTypeIds should have changed.
-  EXPECT_NE(old_email_schema_type_id, new_email_schema_type_id);
+  // SchemaTypeId for message should have changed.
   EXPECT_NE(old_message_schema_type_id, new_message_schema_type_id);
+  EXPECT_EQ(old_email_schema_type_id, new_email_schema_type_id);
 
-  ICING_EXPECT_OK(document_store->UpdateSchemaStore(schema_store.get()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateSchemaStoreResult update_result,
+      document_store->UpdateSchemaStore(schema_store.get()));
+  // - No documents should be deleted since 'dummy' type did not have any
+  //   documents.
+  // - The derived files should be changed.
+  EXPECT_THAT(update_result.deleted_document_count, Eq(0));
+  EXPECT_THAT(update_result.derived_files_changed, IsTrue());
 
   // Check that the FilterCache holds the new SchemaTypeIds
   ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
       email_data,
       document_store->GetAliveDocumentFilterData(
           email_document_id, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(email_data.uri_fingerprint(),
-              Eq(tc3farmhash::Fingerprint64(email_document.uri())));
   EXPECT_THAT(email_data.schema_type_id(), Eq(new_email_schema_type_id));
 
   ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
       message_data,
       document_store->GetAliveDocumentFilterData(
           message_document_id, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(message_data.uri_fingerprint(),
-              Eq(tc3farmhash::Fingerprint64(message_document.uri())));
   EXPECT_THAT(message_data.schema_type_id(), Eq(new_message_schema_type_id));
 }
 
@@ -3241,10 +4570,11 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_without_subject_put_result,
-      document_store->Put(DocumentProto(email_without_subject)));
+      document_store->Put(
+          document_util::CreateDocumentWrapper(email_without_subject)));
   EXPECT_THAT(email_without_subject_put_result.old_document_id,
               Eq(kInvalidDocumentId));
-  EXPECT_FALSE(email_without_subject_put_result.was_replacement());
+  EXPECT_FALSE(email_without_subject_put_result.was_replacement);
   DocumentId email_without_subject_document_id =
       email_without_subject_put_result.new_document_id;
   EXPECT_THAT(document_store->Get(email_without_subject_document_id),
@@ -3252,10 +4582,11 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_with_subject_put_result,
-      document_store->Put(DocumentProto(email_with_subject)));
+      document_store->Put(
+          document_util::CreateDocumentWrapper(email_with_subject)));
   EXPECT_THAT(email_with_subject_put_result.old_document_id,
               Eq(kInvalidDocumentId));
-  EXPECT_FALSE(email_with_subject_put_result.was_replacement());
+  EXPECT_FALSE(email_with_subject_put_result.was_replacement);
   DocumentId email_with_subject_document_id =
       email_with_subject_put_result.new_document_id;
   EXPECT_THAT(document_store->Get(email_with_subject_document_id),
@@ -3269,7 +4600,14 @@
   ICING_EXPECT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/true));
 
-  ICING_EXPECT_OK(document_store->UpdateSchemaStore(schema_store.get()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateSchemaStoreResult update_result,
+      document_store->UpdateSchemaStore(schema_store.get()));
+  // - email_without_subject should be deleted since the new required property
+  //   is missing.
+  // - The derived files should be changed.
+  EXPECT_THAT(update_result.deleted_document_count, Eq(1));
+  EXPECT_THAT(update_result.derived_files_changed, IsTrue());
 
   // The email without a subject should be marked as deleted
   EXPECT_THAT(document_store->Get(email_without_subject_document_id),
@@ -3323,18 +4661,22 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult email_put_result,
-                             document_store->Put(email_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult email_put_result,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(email_document)));
   EXPECT_THAT(email_put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(email_put_result.was_replacement());
+  EXPECT_FALSE(email_put_result.was_replacement);
   DocumentId email_document_id = email_put_result.new_document_id;
   EXPECT_THAT(document_store->Get(email_document_id),
               IsOkAndHolds(EqualsProto(email_document)));
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult message_put_result,
-                             document_store->Put(message_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult message_put_result,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(message_document)));
   EXPECT_THAT(message_put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(message_put_result.was_replacement());
+  EXPECT_FALSE(message_put_result.was_replacement);
   DocumentId message_document_id = message_put_result.new_document_id;
   EXPECT_THAT(document_store->Get(message_document_id),
               IsOkAndHolds(EqualsProto(message_document)));
@@ -3348,7 +4690,13 @@
       schema_store->SetSchema(new_schema,
                               /*ignore_errors_and_delete_documents=*/true));
 
-  ICING_EXPECT_OK(document_store->UpdateSchemaStore(schema_store.get()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateSchemaStoreResult update_result,
+      document_store->UpdateSchemaStore(schema_store.get()));
+  // - email should be deleted.
+  // - The derived files should be changed.
+  EXPECT_THAT(update_result.deleted_document_count, Eq(1));
+  EXPECT_THAT(update_result.derived_files_changed, IsTrue());
 
   // The "email" type is unknown now, so the "email" document should be deleted
   EXPECT_THAT(document_store->Get(email_document_id),
@@ -3359,6 +4707,80 @@
               IsOkAndHolds(EqualsProto(message_document)));
 }
 
+TEST_P(DocumentStoreTest, UpdateSchemaStoreWithFullyCompatibleDocumentStore) {
+  const std::string schema_store_dir = test_dir_ + "_custom";
+  filesystem_.DeleteDirectoryRecursively(schema_store_dir.c_str());
+  filesystem_.CreateDirectoryRecursively(schema_store_dir.c_str());
+
+  // Set a schema with "email" and "message" types.
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("email"))
+          .AddType(SchemaTypeConfigBuilder().SetType("message"))
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
+  ICING_EXPECT_OK(schema_store->SetSchema(
+      schema, /*ignore_errors_and_delete_documents=*/false));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store.get()));
+  std::unique_ptr<DocumentStore> document_store =
+      std::move(create_result.document_store);
+
+  // Add "email" document
+  DocumentProto email_document = DocumentBuilder()
+                                     .SetNamespace("namespace")
+                                     .SetUri("email_uri")
+                                     .SetSchema("email")
+                                     .SetCreationTimestampMs(0)
+                                     .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult email_put_result,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(email_document)));
+  EXPECT_THAT(email_put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(email_put_result.was_replacement);
+  DocumentId email_document_id = email_put_result.new_document_id;
+  EXPECT_THAT(document_store->Get(email_document_id),
+              IsOkAndHolds(EqualsProto(email_document)));
+
+  // Delete schema type "message".
+  // - "email" type should have the same schema type id.
+  // - There was no document with "message" schema type, so no document should
+  //   be deleted.
+  //
+  // Therefore, the new schema is fully compatible with the DocumentStore.
+  // Derived files should not be changed.
+  SchemaProto new_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("email"))
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaStore::SetSchemaResult set_schema_result,
+      schema_store->SetSchema(new_schema,
+                              /*ignore_errors_and_delete_documents=*/true));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateSchemaStoreResult update_result,
+      document_store->UpdateSchemaStore(schema_store.get()));
+  // - No document is deleted since there were no documents with "message"
+  //   schema type.
+  // - The derived files should be UNCHANGED.
+  EXPECT_THAT(update_result.deleted_document_count, Eq(0));
+  EXPECT_THAT(update_result.derived_files_changed, IsFalse());
+
+  // The "email" document should be unaffected
+  EXPECT_THAT(document_store->Get(email_document_id),
+              IsOkAndHolds(EqualsProto(email_document)));
+}
+
 TEST_P(DocumentStoreTest, OptimizedUpdateSchemaStoreUpdatesSchemaTypeIds) {
   const std::string schema_store_dir = test_dir_ + "_custom";
   filesystem_.DeleteDirectoryRecursively(schema_store_dir.c_str());
@@ -3367,6 +4789,7 @@
   // Set a schema
   SchemaProto schema =
       SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("dummy"))
           .AddType(SchemaTypeConfigBuilder().SetType("email"))
           .AddType(SchemaTypeConfigBuilder().SetType("message"))
           .Build();
@@ -3403,36 +4826,35 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult email_put_result,
-                             document_store->Put(email_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult email_put_result,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(email_document)));
   EXPECT_THAT(email_put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(email_put_result.was_replacement());
+  EXPECT_FALSE(email_put_result.was_replacement);
   DocumentId email_document_id = email_put_result.new_document_id;
   ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
       DocumentFilterData email_data,
       document_store->GetAliveDocumentFilterData(
           email_document_id, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(email_data.uri_fingerprint(),
-              Eq(tc3farmhash::Fingerprint64(email_document.uri())));
   EXPECT_THAT(email_data.schema_type_id(), Eq(old_email_schema_type_id));
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult message_put_result,
-                             document_store->Put(message_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult message_put_result,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(message_document)));
   EXPECT_THAT(message_put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(message_put_result.was_replacement());
+  EXPECT_FALSE(message_put_result.was_replacement);
   DocumentId message_document_id = message_put_result.new_document_id;
   ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
       DocumentFilterData message_data,
       document_store->GetAliveDocumentFilterData(
           message_document_id, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(message_data.uri_fingerprint(),
-              Eq(tc3farmhash::Fingerprint64(message_document.uri())));
   EXPECT_THAT(message_data.schema_type_id(), Eq(old_message_schema_type_id));
 
-  // Add a new schema type. Since SchemaTypeId is assigned based on order,
-  // this should change the SchemaTypeIds.
+  // Delete the first type. With schema type id optimization, this should change
+  // the type id of "message" but not "email."
   schema = SchemaBuilder()
-               .AddType(SchemaTypeConfigBuilder().SetType("newType"))
                .AddType(SchemaTypeConfigBuilder().SetType("email"))
                .AddType(SchemaTypeConfigBuilder().SetType("message"))
                .Build();
@@ -3440,35 +4862,38 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       SchemaStore::SetSchemaResult set_schema_result,
       schema_store->SetSchema(schema,
-                              /*ignore_errors_and_delete_documents=*/false));
+                              /*ignore_errors_and_delete_documents=*/true));
 
   ICING_ASSERT_OK_AND_ASSIGN(SchemaTypeId new_email_schema_type_id,
                              schema_store->GetSchemaTypeId("email"));
   ICING_ASSERT_OK_AND_ASSIGN(SchemaTypeId new_message_schema_type_id,
                              schema_store->GetSchemaTypeId("message"));
 
-  // SchemaTypeIds should have changed.
-  EXPECT_NE(old_email_schema_type_id, new_email_schema_type_id);
+  // SchemaTypeId for 'message' should have changed.
   EXPECT_NE(old_message_schema_type_id, new_message_schema_type_id);
+  EXPECT_EQ(old_email_schema_type_id, new_email_schema_type_id);
 
-  ICING_EXPECT_OK(document_store->OptimizedUpdateSchemaStore(
-      schema_store.get(), set_schema_result));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateSchemaStoreResult update_result,
+      document_store->OptimizedUpdateSchemaStore(schema_store.get(),
+                                                 set_schema_result));
+  // - No documents should be deleted since 'dummy' type did not have any
+  //   documents.
+  // - The derived files should be changed.
+  EXPECT_THAT(update_result.deleted_document_count, Eq(0));
+  EXPECT_THAT(update_result.derived_files_changed, IsTrue());
 
   // Check that the FilterCache holds the new SchemaTypeIds
   ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
       email_data,
       document_store->GetAliveDocumentFilterData(
           email_document_id, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(email_data.uri_fingerprint(),
-              Eq(tc3farmhash::Fingerprint64(email_document.uri())));
   EXPECT_THAT(email_data.schema_type_id(), Eq(new_email_schema_type_id));
 
   ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
       message_data,
       document_store->GetAliveDocumentFilterData(
           message_document_id, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(message_data.uri_fingerprint(),
-              Eq(tc3farmhash::Fingerprint64(message_document.uri())));
   EXPECT_THAT(message_data.schema_type_id(), Eq(new_message_schema_type_id));
 }
 
@@ -3520,10 +4945,11 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_without_subject_put_result,
-      document_store->Put(email_without_subject));
+      document_store->Put(
+          document_util::CreateDocumentWrapper(email_without_subject)));
   EXPECT_THAT(email_without_subject_put_result.old_document_id,
               Eq(kInvalidDocumentId));
-  EXPECT_FALSE(email_without_subject_put_result.was_replacement());
+  EXPECT_FALSE(email_without_subject_put_result.was_replacement);
   DocumentId email_without_subject_document_id =
       email_without_subject_put_result.new_document_id;
   EXPECT_THAT(document_store->Get(email_without_subject_document_id),
@@ -3531,10 +4957,11 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_with_subject_put_result,
-      document_store->Put(email_with_subject));
+      document_store->Put(
+          document_util::CreateDocumentWrapper(email_with_subject)));
   EXPECT_THAT(email_with_subject_put_result.old_document_id,
               Eq(kInvalidDocumentId));
-  EXPECT_FALSE(email_with_subject_put_result.was_replacement());
+  EXPECT_FALSE(email_with_subject_put_result.was_replacement);
   DocumentId email_with_subject_document_id =
       email_with_subject_put_result.new_document_id;
   EXPECT_THAT(document_store->Get(email_with_subject_document_id),
@@ -3550,8 +4977,15 @@
       schema_store->SetSchema(schema,
                               /*ignore_errors_and_delete_documents=*/true));
 
-  ICING_EXPECT_OK(document_store->OptimizedUpdateSchemaStore(
-      schema_store.get(), set_schema_result));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateSchemaStoreResult update_result,
+      document_store->OptimizedUpdateSchemaStore(schema_store.get(),
+                                                 set_schema_result));
+  // - email_without_subject should be deleted since the new required property
+  //   is missing.
+  // - The derived files should be changed.
+  EXPECT_THAT(update_result.deleted_document_count, Eq(1));
+  EXPECT_THAT(update_result.derived_files_changed, IsTrue());
 
   // The email without a subject should be marked as deleted
   EXPECT_THAT(document_store->Get(email_without_subject_document_id),
@@ -3605,18 +5039,22 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult email_put_result,
-                             document_store->Put(email_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult email_put_result,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(email_document)));
   EXPECT_THAT(email_put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(email_put_result.was_replacement());
+  EXPECT_FALSE(email_put_result.was_replacement);
   DocumentId email_document_id = email_put_result.new_document_id;
   EXPECT_THAT(document_store->Get(email_document_id),
               IsOkAndHolds(EqualsProto(email_document)));
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult message_put_result,
-                             document_store->Put(message_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult message_put_result,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(message_document)));
   EXPECT_THAT(message_put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(message_put_result.was_replacement());
+  EXPECT_FALSE(message_put_result.was_replacement);
   DocumentId message_document_id = message_put_result.new_document_id;
   EXPECT_THAT(document_store->Get(message_document_id),
               IsOkAndHolds(EqualsProto(message_document)));
@@ -3631,8 +5069,14 @@
       schema_store->SetSchema(new_schema,
                               /*ignore_errors_and_delete_documents=*/true));
 
-  ICING_EXPECT_OK(document_store->OptimizedUpdateSchemaStore(
-      schema_store.get(), set_schema_result));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateSchemaStoreResult update_result,
+      document_store->OptimizedUpdateSchemaStore(schema_store.get(),
+                                                 set_schema_result));
+  // - email should be deleted.
+  // - The derived files should be changed.
+  EXPECT_THAT(update_result.deleted_document_count, Eq(1));
+  EXPECT_THAT(update_result.derived_files_changed, IsTrue());
 
   // The "email" type is unknown now, so the "email" document should be deleted
   EXPECT_THAT(document_store->Get(email_document_id),
@@ -3643,6 +5087,82 @@
               IsOkAndHolds(EqualsProto(message_document)));
 }
 
+TEST_P(DocumentStoreTest,
+       OptimizedUpdateSchemaStoreWithFullyCompatibleDocumentStore) {
+  const std::string schema_store_dir = test_dir_ + "_custom";
+  filesystem_.DeleteDirectoryRecursively(schema_store_dir.c_str());
+  filesystem_.CreateDirectoryRecursively(schema_store_dir.c_str());
+
+  // Set a schema with "email" and "message" types.
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("email"))
+          .AddType(SchemaTypeConfigBuilder().SetType("message"))
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
+  ICING_EXPECT_OK(schema_store->SetSchema(
+      schema, /*ignore_errors_and_delete_documents=*/false));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store.get()));
+  std::unique_ptr<DocumentStore> document_store =
+      std::move(create_result.document_store);
+
+  // Add "email" document
+  DocumentProto email_document = DocumentBuilder()
+                                     .SetNamespace("namespace")
+                                     .SetUri("email_uri")
+                                     .SetSchema("email")
+                                     .SetCreationTimestampMs(0)
+                                     .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult email_put_result,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(email_document)));
+  EXPECT_THAT(email_put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(email_put_result.was_replacement);
+  DocumentId email_document_id = email_put_result.new_document_id;
+  EXPECT_THAT(document_store->Get(email_document_id),
+              IsOkAndHolds(EqualsProto(email_document)));
+
+  // Delete schema type "message".
+  // - "email" type should have the same schema type id.
+  // - There was no document with "message" schema type, so no document should
+  //   be deleted.
+  //
+  // Therefore, the new schema is fully compatible with the DocumentStore.
+  // Derived files should not be changed.
+  SchemaProto new_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("email"))
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaStore::SetSchemaResult set_schema_result,
+      schema_store->SetSchema(new_schema,
+                              /*ignore_errors_and_delete_documents=*/true));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::UpdateSchemaStoreResult update_result,
+      document_store->OptimizedUpdateSchemaStore(schema_store.get(),
+                                                 set_schema_result));
+  // - No document is deleted since there were no documents with "message"
+  //   schema type.
+  // - The derived files should be UNCHANGED.
+  EXPECT_THAT(update_result.deleted_document_count, Eq(0));
+  EXPECT_THAT(update_result.derived_files_changed, IsFalse());
+
+  // The "email" document should be unaffected
+  EXPECT_THAT(document_store->Get(email_document_id),
+              IsOkAndHolds(EqualsProto(email_document)));
+}
+
 TEST_P(DocumentStoreTest, GetOptimizeInfo) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
@@ -3658,7 +5178,8 @@
   EXPECT_THAT(optimize_info.optimizable_docs, Eq(0));
   EXPECT_THAT(optimize_info.estimated_optimizable_bytes, Eq(0));
 
-  ICING_EXPECT_OK(document_store->Put(DocumentProto(test_document1_)));
+  ICING_EXPECT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(test_document1_)));
 
   // Adding a document, still nothing is optimizable
   ICING_ASSERT_OK_AND_ASSIGN(optimize_info, document_store->GetOptimizeInfo());
@@ -3733,10 +5254,14 @@
                                  .SetTtlMs(100)
                                  .Build();
 
-  ICING_ASSERT_OK(document_store->Put(namespace1));
-  ICING_ASSERT_OK(document_store->Put(namespace2_uri1));
-  ICING_ASSERT_OK(document_store->Put(namespace2_uri2));
-  ICING_ASSERT_OK(document_store->Put(namespace3));
+  ICING_ASSERT_OK(
+      document_store->Put(document_util::CreateDocumentWrapper(namespace1)));
+  ICING_ASSERT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(namespace2_uri1)));
+  ICING_ASSERT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(namespace2_uri2)));
+  ICING_ASSERT_OK(
+      document_store->Put(document_util::CreateDocumentWrapper(namespace3)));
 
   auto get_result = document_store->Get("namespace1", "uri");
   get_result = document_store->Get("namespace2", "uri1");
@@ -3777,10 +5302,12 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store->Put(test_document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(test_document1_)));
   EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result1.was_replacement());
+  EXPECT_FALSE(put_result1.was_replacement);
   DocumentId document_id = put_result1.new_document_id;
 
   // Report usage with type 1 and time 1.
@@ -3872,10 +5399,12 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store->Put(test_document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(test_document1_)));
   EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result1.was_replacement());
+  EXPECT_FALSE(put_result1.was_replacement);
   DocumentId document_id = put_result1.new_document_id;
 
   // Report usage with type 1.
@@ -3928,10 +5457,12 @@
     std::unique_ptr<DocumentStore> document_store =
         std::move(create_result.document_store);
 
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                               document_store->Put(test_document1_));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult put_result1,
+        document_store->Put(
+            document_util::CreateDocumentWrapper(test_document1_)));
     EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_result1.was_replacement());
+    EXPECT_FALSE(put_result1.was_replacement);
     document_id = put_result1.new_document_id;
 
     // Report usage with type 1.
@@ -3976,10 +5507,12 @@
     std::unique_ptr<DocumentStore> document_store =
         std::move(create_result.document_store);
 
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                               document_store->Put(test_document1_));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult put_result1,
+        document_store->Put(
+            document_util::CreateDocumentWrapper(test_document1_)));
     EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_result1.was_replacement());
+    EXPECT_FALSE(put_result1.was_replacement);
     document_id = put_result1.new_document_id;
 
     // Report usage with type 1.
@@ -4032,10 +5565,12 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store->Put(test_document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(test_document1_)));
   EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result1.was_replacement());
+  EXPECT_FALSE(put_result1.was_replacement);
   DocumentId document_id = put_result1.new_document_id;
 
   // Report usage with type 1.
@@ -4053,10 +5588,12 @@
   EXPECT_THAT(actual_scores, Eq(expected_scores));
 
   // Update the document.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store->Put(test_document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(test_document1_)));
   EXPECT_THAT(put_result2.old_document_id, Eq(document_id));
-  EXPECT_TRUE(put_result2.was_replacement());
+  EXPECT_TRUE(put_result2.was_replacement);
   DocumentId updated_document_id = put_result2.new_document_id;
   // We should get a different document id.
   ASSERT_THAT(updated_document_id, Not(Eq(document_id)));
@@ -4077,15 +5614,19 @@
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store->Put(test_document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(test_document1_)));
   EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result1.was_replacement());
+  EXPECT_FALSE(put_result1.was_replacement);
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store->Put(test_document2_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(test_document2_)));
   EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result2.was_replacement());
+  EXPECT_FALSE(put_result2.was_replacement);
   DocumentId document_id2 = put_result2.new_document_id;
   ICING_ASSERT_OK(document_store->Delete(
       document_id1, fake_clock_.GetSystemTimeMilliseconds()));
@@ -4136,10 +5677,12 @@
       std::move(create_result.document_store);
 
   // Add the document.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store->Put(test_document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(test_document1_)));
   EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result1.was_replacement());
+  EXPECT_FALSE(put_result1.was_replacement);
   DocumentId document_id = put_result1.new_document_id;
 
   // Delete the document.
@@ -4147,12 +5690,14 @@
       document_id, fake_clock_.GetSystemTimeMilliseconds()));
 
   // Re-add the document.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store->Put(test_document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(test_document1_)));
 
   // Because the document was deleted, it should not be a replacement.
-  EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result2.was_replacement());
+  EXPECT_THAT(put_result2.old_document_id, Eq(0));
+  EXPECT_FALSE(put_result2.was_replacement);
   DocumentId updated_document_id = put_result2.new_document_id;
   ASSERT_THAT(updated_document_id, Not(Eq(document_id)));
 }
@@ -4168,10 +5713,11 @@
   // Add the document.
   DocumentProto doc1 = test_document1_;
   doc1.set_ttl_ms(1);
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             document_store->Put(doc1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      document_store->Put(document_util::CreateDocumentWrapper(doc1)));
   EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result1.was_replacement());
+  EXPECT_FALSE(put_result1.was_replacement);
   DocumentId document_id = put_result1.new_document_id;
 
   // Expire the document by advancing the clock by two milliseconds.
@@ -4179,12 +5725,14 @@
       fake_clock_.GetSystemTimeMilliseconds() + 2);
 
   // Re-add the document.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             document_store->Put(test_document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store->Put(
+          document_util::CreateDocumentWrapper(test_document1_)));
 
   // Because the document was expired, it should not be a replacement.
-  EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result2.was_replacement());
+  EXPECT_THAT(put_result2.old_document_id, Eq(0));
+  EXPECT_FALSE(put_result2.was_replacement);
   DocumentId updated_document_id = put_result2.new_document_id;
   ASSERT_THAT(updated_document_id, Not(Eq(document_id)));
 }
@@ -4201,10 +5749,11 @@
     EXPECT_THAT(create_result.data_loss, Eq(DataLoss::NONE));
     EXPECT_THAT(create_result.derived_files_regenerated, IsFalse());
 
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                               doc_store->Put(test_document1_));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult put_result,
+        doc_store->Put(document_util::CreateDocumentWrapper(test_document1_)));
     EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_result.was_replacement());
+    EXPECT_FALSE(put_result.was_replacement);
     DocumentId document_id = put_result.new_document_id;
     EXPECT_THAT(doc_store->Get(document_id),
                 IsOkAndHolds(EqualsProto(test_document1_)));
@@ -4257,10 +5806,11 @@
     // initialization.
     corruptible_offset = filesystem_.GetFileSize(document_log_file.c_str());
 
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                               doc_store->Put(test_document1_));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult put_result,
+        doc_store->Put(document_util::CreateDocumentWrapper(test_document1_)));
     EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_result.was_replacement());
+    EXPECT_FALSE(put_result.was_replacement);
     DocumentId document_id = put_result.new_document_id;
     EXPECT_THAT(doc_store->Get(document_id),
                 IsOkAndHolds(EqualsProto(test_document1_)));
@@ -4289,7 +5839,8 @@
 
     // Set dirty bit to true to reflect that something changed in the log.
     header.SetDirtyFlag(true);
-    header.SetHeaderChecksum(header.CalculateHeaderChecksum());
+    header.UpdateHeaderChecksums(
+        feature_flags_->enable_proto_log_new_header_format());
 
     WriteDocumentLogHeader(filesystem_, document_log_file, header);
   }
@@ -4350,7 +5901,9 @@
           /*force_recovery_and_revalidate_documents=*/false,
           GetParam().pre_mapping_fbv, GetParam().use_persistent_hash_map,
           PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
-          &initialize_stats));
+          PortableFileBackedProtoLog<
+              DocumentWrapper>::kDefaultCompressionThresholdBytes,
+          protobuf_ports::kDefaultMemLevel, &initialize_stats));
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
   // The document log is using the legacy v0 format so that a migration is
@@ -4375,24 +5928,28 @@
   DocumentProto document1 = test_document1_;
   document1.set_namespace_("namespace.1");
   document1.set_uri("uri1");
-  ICING_ASSERT_OK(doc_store->Put(document1));
+  ICING_ASSERT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(document1)));
 
   DocumentProto document2 = test_document1_;
   document2.set_namespace_("namespace.1");
   document2.set_uri("uri2");
   document2.set_creation_timestamp_ms(fake_clock_.GetSystemTimeMilliseconds());
   document2.set_ttl_ms(100);
-  ICING_ASSERT_OK(doc_store->Put(document2));
+  ICING_ASSERT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(document2)));
 
   DocumentProto document3 = test_document1_;
   document3.set_namespace_("namespace.1");
   document3.set_uri("uri3");
-  ICING_ASSERT_OK(doc_store->Put(document3));
+  ICING_ASSERT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(document3)));
 
   DocumentProto document4 = test_document1_;
   document4.set_namespace_("namespace.2");
   document4.set_uri("uri1");
-  ICING_ASSERT_OK(doc_store->Put(document4));
+  ICING_ASSERT_OK(
+      doc_store->Put(document_util::CreateDocumentWrapper(document4)));
 
   // Report usage with type 1 on document1
   UsageReport usage_report_type1 = CreateUsageReport(
@@ -4492,7 +6049,10 @@
                            .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
                            .SetCardinality(CARDINALITY_OPTIONAL))
           .Build();
-  SchemaProto schema = SchemaBuilder().AddType(email_type_config).Build();
+  SchemaProto schema = SchemaBuilder()
+                           .AddType(SchemaTypeConfigBuilder().SetType("dummy"))
+                           .AddType(email_type_config)
+                           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
       SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
@@ -4500,8 +6060,9 @@
   ASSERT_THAT(schema_store->SetSchema(
                   schema, /*ignore_errors_and_delete_documents=*/false),
               IsOk());
-  // The typeid for "email" should be 0.
-  ASSERT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(0));
+  // The typeid for "dummy" should be 0 and "email" should be 1.
+  ASSERT_THAT(schema_store->GetSchemaTypeId("dummy"), IsOkAndHolds(0));
+  ASSERT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(1));
 
   DocumentId docid = kInvalidDocumentId;
   {
@@ -4524,58 +6085,43 @@
                 document1_creation_timestamp_)  // A random timestamp
             .SetTtlMs(document1_ttl_)
             .Build();
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                               doc_store->Put(doc));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult put_result,
+        doc_store->Put(document_util::CreateDocumentWrapper(doc)));
     EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_result.was_replacement());
+    EXPECT_FALSE(put_result.was_replacement);
     docid = put_result.new_document_id;
     ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
         DocumentFilterData filter_data,
         doc_store->GetAliveDocumentFilterData(
             docid, fake_clock_.GetSystemTimeMilliseconds()));
 
-    ASSERT_THAT(filter_data.uri_fingerprint(),
-                Eq(tc3farmhash::Fingerprint64(doc.uri())));
-    ASSERT_THAT(filter_data.schema_type_id(), Eq(0));
+    ASSERT_THAT(filter_data.schema_type_id(), Eq(1));
   }
 
-  // Add another type to the schema before the email type.
-  schema =
-      SchemaBuilder()
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("alarm")
-                       .AddProperty(PropertyConfigBuilder()
-                                        .SetName("name")
-                                        .SetDataTypeString(TERM_MATCH_EXACT,
-                                                           TOKENIZER_PLAIN)
-                                        .SetCardinality(CARDINALITY_OPTIONAL))
-                       .AddProperty(PropertyConfigBuilder()
-                                        .SetName("time")
-                                        .SetDataType(TYPE_INT64)
-                                        .SetCardinality(CARDINALITY_OPTIONAL)))
-          .AddType(email_type_config)
-          .Build();
+  // Delete the dummy type from the schema.
+  schema = SchemaBuilder().AddType(email_type_config).Build();
   ASSERT_THAT(schema_store->SetSchema(
-                  schema, /*ignore_errors_and_delete_documents=*/false),
+                  schema, /*ignore_errors_and_delete_documents=*/true),
               IsOk());
-  // Adding a new type should cause ids to be reassigned. Ids are assigned in
-  // order of appearance so 'alarm' should be 0 and 'email' should be 1.
-  ASSERT_THAT(schema_store->GetSchemaTypeId("alarm"), IsOkAndHolds(0));
-  ASSERT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(1));
+  // Deleting the dummy type will cause email's type id to be reassigned to 0.
+  ASSERT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(0));
 
   {
     // Create the document store the second time and force recovery
     InitializeStatsProto initialize_stats;
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, document_store_dir_, &fake_clock_,
-                              schema_store.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/true,
-                              GetParam().pre_mapping_fbv,
-                              GetParam().use_persistent_hash_map,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              &initialize_stats));
+        DocumentStore::Create(
+            &filesystem_, document_store_dir_, &fake_clock_, schema_store.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/true,
+            GetParam().pre_mapping_fbv, GetParam().use_persistent_hash_map,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel, &initialize_stats));
     std::unique_ptr<DocumentStore> doc_store =
         std::move(create_result.document_store);
 
@@ -4584,9 +6130,7 @@
         DocumentFilterData filter_data,
         doc_store->GetAliveDocumentFilterData(
             docid, fake_clock_.GetSystemTimeMilliseconds()));
-    EXPECT_THAT(filter_data.uri_fingerprint(),
-                Eq(tc3farmhash::Fingerprint64(std::string("email/1"))));
-    EXPECT_THAT(filter_data.schema_type_id(), Eq(1));
+    EXPECT_THAT(filter_data.schema_type_id(), Eq(0));
     EXPECT_THAT(initialize_stats.document_store_recovery_cause(),
                 Eq(InitializeStatsProto::SCHEMA_CHANGES_OUT_OF_SYNC));
   }
@@ -4611,7 +6155,10 @@
                            .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
                            .SetCardinality(CARDINALITY_OPTIONAL))
           .Build();
-  SchemaProto schema = SchemaBuilder().AddType(email_type_config).Build();
+  SchemaProto schema = SchemaBuilder()
+                           .AddType(SchemaTypeConfigBuilder().SetType("dummy"))
+                           .AddType(email_type_config)
+                           .Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
       SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
@@ -4619,8 +6166,9 @@
   ASSERT_THAT(schema_store->SetSchema(
                   schema, /*ignore_errors_and_delete_documents=*/false),
               IsOk());
-  // The typeid for "email" should be 0.
-  ASSERT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(0));
+  // The typeid for "dummy" should be 0 and "email" should be 1.
+  ASSERT_THAT(schema_store->GetSchemaTypeId("dummy"), IsOkAndHolds(0));
+  ASSERT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(1));
 
   DocumentId docid = kInvalidDocumentId;
   {
@@ -4643,44 +6191,28 @@
                 document1_creation_timestamp_)  // A random timestamp
             .SetTtlMs(document1_ttl_)
             .Build();
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                               doc_store->Put(doc));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult put_result,
+        doc_store->Put(document_util::CreateDocumentWrapper(doc)));
     EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_result.was_replacement());
+    EXPECT_FALSE(put_result.was_replacement);
     docid = put_result.new_document_id;
     ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
         DocumentFilterData filter_data,
         doc_store->GetAliveDocumentFilterData(
             docid, fake_clock_.GetSystemTimeMilliseconds()));
 
-    EXPECT_THAT(filter_data.uri_fingerprint(),
-                Eq(tc3farmhash::Fingerprint64(doc.uri())));
-    ASSERT_THAT(filter_data.schema_type_id(), Eq(0));
+    ASSERT_THAT(filter_data.schema_type_id(), Eq(1));
   }
 
-  // Add another type to the schema.
-  schema =
-      SchemaBuilder()
-          .AddType(SchemaTypeConfigBuilder()
-                       .SetType("alarm")
-                       .AddProperty(PropertyConfigBuilder()
-                                        .SetName("name")
-                                        .SetDataTypeString(TERM_MATCH_EXACT,
-                                                           TOKENIZER_PLAIN)
-                                        .SetCardinality(CARDINALITY_OPTIONAL))
-                       .AddProperty(PropertyConfigBuilder()
-                                        .SetName("time")
-                                        .SetDataType(TYPE_INT64)
-                                        .SetCardinality(CARDINALITY_OPTIONAL)))
-          .AddType(email_type_config)
-          .Build();
+  // Delete the dummy type from the schema.
+  schema = SchemaBuilder().AddType(email_type_config).Build();
   ASSERT_THAT(schema_store->SetSchema(
-                  schema, /*ignore_errors_and_delete_documents=*/false),
+                  schema, /*ignore_errors_and_delete_documents=*/true),
               IsOk());
-  // Adding a new type should cause ids to be reassigned. Ids are assigned in
-  // order of appearance so 'alarm' should be 0 and 'email' should be 1.
-  ASSERT_THAT(schema_store->GetSchemaTypeId("alarm"), IsOkAndHolds(0));
-  ASSERT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(1));
+  // Deleting the dummy type will cause email's type id to be reassigned to 0.
+
+  ASSERT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(0));
 
   {
     // Create the document store the second time. Don't force recovery.
@@ -4696,9 +6228,7 @@
         DocumentFilterData filter_data,
         doc_store->GetAliveDocumentFilterData(
             docid, fake_clock_.GetSystemTimeMilliseconds()));
-    EXPECT_THAT(filter_data.uri_fingerprint(),
-                Eq(tc3farmhash::Fingerprint64(std::string("email/1"))));
-    ASSERT_THAT(filter_data.schema_type_id(), Eq(0));
+    ASSERT_THAT(filter_data.schema_type_id(), Eq(1));
   }
 }
 
@@ -4730,7 +6260,7 @@
                   schema, /*ignore_errors_and_delete_documents=*/false),
               IsOk());
 
-  DocumentProto docWithBody =
+  DocumentProto doc_with_body =
       DocumentBuilder()
           .SetKey("icing", "email/1")
           .SetSchema("email")
@@ -4741,7 +6271,7 @@
               document1_creation_timestamp_)  // A random timestamp
           .SetTtlMs(document1_ttl_)
           .Build();
-  DocumentProto docWithoutBody =
+  DocumentProto doc_without_body =
       DocumentBuilder()
           .SetKey("icing", "email/2")
           .SetSchema("email")
@@ -4763,26 +6293,28 @@
         std::move(create_result.document_store);
 
     DocumentId docid = kInvalidDocumentId;
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_with_body_result,
-                               doc_store->Put(docWithBody));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult put_with_body_result,
+        doc_store->Put(document_util::CreateDocumentWrapper(doc_with_body)));
     EXPECT_THAT(put_with_body_result.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_with_body_result.was_replacement());
+    EXPECT_FALSE(put_with_body_result.was_replacement);
     docid = put_with_body_result.new_document_id;
     ASSERT_NE(docid, kInvalidDocumentId);
     docid = kInvalidDocumentId;
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_without_body_result,
-                               doc_store->Put(docWithoutBody));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult put_without_body_result,
+        doc_store->Put(document_util::CreateDocumentWrapper(doc_without_body)));
     EXPECT_THAT(put_without_body_result.old_document_id,
                 Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_without_body_result.was_replacement());
+    EXPECT_FALSE(put_without_body_result.was_replacement);
     docid = put_without_body_result.new_document_id;
     ASSERT_NE(docid, kInvalidDocumentId);
 
-    ASSERT_THAT(doc_store->Get(docWithBody.namespace_(), docWithBody.uri()),
-                IsOkAndHolds(EqualsProto(docWithBody)));
+    ASSERT_THAT(doc_store->Get(doc_with_body.namespace_(), doc_with_body.uri()),
+                IsOkAndHolds(EqualsProto(doc_with_body)));
     ASSERT_THAT(
-        doc_store->Get(docWithoutBody.namespace_(), docWithoutBody.uri()),
-        IsOkAndHolds(EqualsProto(docWithoutBody)));
+        doc_store->Get(doc_without_body.namespace_(), doc_without_body.uri()),
+        IsOkAndHolds(EqualsProto(doc_without_body)));
   }
 
   // Delete the 'body' property from the 'email' type, making all pre-existing
@@ -4805,22 +6337,25 @@
     CorruptDocStoreHeaderChecksumFile();
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, document_store_dir_, &fake_clock_,
-                              schema_store.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/true,
-                              GetParam().pre_mapping_fbv,
-                              GetParam().use_persistent_hash_map,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, document_store_dir_, &fake_clock_, schema_store.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/true,
+            GetParam().pre_mapping_fbv, GetParam().use_persistent_hash_map,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     std::unique_ptr<DocumentStore> doc_store =
         std::move(create_result.document_store);
 
-    ASSERT_THAT(doc_store->Get(docWithBody.namespace_(), docWithBody.uri()),
+    ASSERT_THAT(doc_store->Get(doc_with_body.namespace_(), doc_with_body.uri()),
                 StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
     ASSERT_THAT(
-        doc_store->Get(docWithoutBody.namespace_(), docWithoutBody.uri()),
-        IsOkAndHolds(EqualsProto(docWithoutBody)));
+        doc_store->Get(doc_without_body.namespace_(), doc_without_body.uri()),
+        IsOkAndHolds(EqualsProto(doc_without_body)));
   }
 }
 
@@ -4852,7 +6387,7 @@
                   schema, /*ignore_errors_and_delete_documents=*/false),
               IsOk());
 
-  DocumentProto docWithBody =
+  DocumentProto doc_with_body =
       DocumentBuilder()
           .SetKey("icing", "email/1")
           .SetSchema("email")
@@ -4863,7 +6398,7 @@
               document1_creation_timestamp_)  // A random timestamp
           .SetTtlMs(document1_ttl_)
           .Build();
-  DocumentProto docWithoutBody =
+  DocumentProto doc_without_body =
       DocumentBuilder()
           .SetKey("icing", "email/2")
           .SetSchema("email")
@@ -4885,26 +6420,28 @@
         std::move(create_result.document_store);
 
     DocumentId docid = kInvalidDocumentId;
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_with_body_result,
-                               doc_store->Put(docWithBody));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult put_with_body_result,
+        doc_store->Put(document_util::CreateDocumentWrapper(doc_with_body)));
     EXPECT_THAT(put_with_body_result.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_with_body_result.was_replacement());
+    EXPECT_FALSE(put_with_body_result.was_replacement);
     docid = put_with_body_result.new_document_id;
     ASSERT_NE(docid, kInvalidDocumentId);
     docid = kInvalidDocumentId;
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_without_body_result,
-                               doc_store->Put(docWithoutBody));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult put_without_body_result,
+        doc_store->Put(document_util::CreateDocumentWrapper(doc_without_body)));
     EXPECT_THAT(put_without_body_result.old_document_id,
                 Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_without_body_result.was_replacement());
+    EXPECT_FALSE(put_without_body_result.was_replacement);
     docid = put_without_body_result.new_document_id;
     ASSERT_NE(docid, kInvalidDocumentId);
 
-    ASSERT_THAT(doc_store->Get(docWithBody.namespace_(), docWithBody.uri()),
-                IsOkAndHolds(EqualsProto(docWithBody)));
+    ASSERT_THAT(doc_store->Get(doc_with_body.namespace_(), doc_with_body.uri()),
+                IsOkAndHolds(EqualsProto(doc_with_body)));
     ASSERT_THAT(
-        doc_store->Get(docWithoutBody.namespace_(), docWithoutBody.uri()),
-        IsOkAndHolds(EqualsProto(docWithoutBody)));
+        doc_store->Get(doc_without_body.namespace_(), doc_without_body.uri()),
+        IsOkAndHolds(EqualsProto(doc_without_body)));
   }
 
   // Delete the 'body' property from the 'email' type, making all pre-existing
@@ -4933,11 +6470,11 @@
     std::unique_ptr<DocumentStore> doc_store =
         std::move(create_result.document_store);
 
-    ASSERT_THAT(doc_store->Get(docWithBody.namespace_(), docWithBody.uri()),
-                IsOkAndHolds(EqualsProto(docWithBody)));
+    ASSERT_THAT(doc_store->Get(doc_with_body.namespace_(), doc_with_body.uri()),
+                IsOkAndHolds(EqualsProto(doc_with_body)));
     ASSERT_THAT(
-        doc_store->Get(docWithoutBody.namespace_(), docWithoutBody.uri()),
-        IsOkAndHolds(EqualsProto(docWithoutBody)));
+        doc_store->Get(doc_without_body.namespace_(), doc_without_body.uri()),
+        IsOkAndHolds(EqualsProto(doc_without_body)));
   }
 }
 
@@ -5012,7 +6549,9 @@
           /*force_recovery_and_revalidate_documents=*/false,
           GetParam().pre_mapping_fbv, GetParam().use_persistent_hash_map,
           PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
-          &initialize_stats));
+          PortableFileBackedProtoLog<
+              DocumentWrapper>::kDefaultCompressionThresholdBytes,
+          protobuf_ports::kDefaultMemLevel, &initialize_stats));
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
@@ -5116,7 +6655,8 @@
                                 .AddStringProperty("body", "dd ee")
                                 .SetCreationTimestampMs(1)
                                 .Build();
-  ICING_ASSERT_OK(document_store->Put(document1, 5));
+  ICING_ASSERT_OK(document_store->Put(
+      CreateDocumentWrapper(document1, /*num_string_tokens=*/5)));
 
   DocumentProto document2 = DocumentBuilder()
                                 .SetKey("namespace2", "email/2")
@@ -5125,7 +6665,8 @@
                                 .AddStringProperty("body", "cc")
                                 .SetCreationTimestampMs(1)
                                 .Build();
-  ICING_ASSERT_OK(document_store->Put(document2, 3));
+  ICING_ASSERT_OK(document_store->Put(
+      CreateDocumentWrapper(document2, /*num_string_tokens=*/3)));
 
   DocumentProto document3 = DocumentBuilder()
                                 .SetKey("namespace2", "email/3")
@@ -5134,7 +6675,8 @@
                                 .AddStringProperty("body", "")
                                 .SetCreationTimestampMs(1)
                                 .Build();
-  ICING_ASSERT_OK(document_store->Put(document3, 1));
+  ICING_ASSERT_OK(document_store->Put(
+      CreateDocumentWrapper(document3, /*num_string_tokens=*/1)));
 
   DocumentProto document4 = DocumentBuilder()
                                 .SetKey("namespace1", "person/1")
@@ -5142,7 +6684,8 @@
                                 .AddStringProperty("name", "test test")
                                 .SetCreationTimestampMs(1)
                                 .Build();
-  ICING_ASSERT_OK(document_store->Put(document4, 2));
+  ICING_ASSERT_OK(document_store->Put(
+      CreateDocumentWrapper(document4, /*num_string_tokens=*/2)));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentDebugInfoProto out1,
@@ -5246,21 +6789,25 @@
   {
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, document_store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              GetParam().pre_mapping_fbv,
-                              GetParam().use_persistent_hash_map,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, document_store_dir_, &fake_clock_,
+            schema_store_.get(), feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            GetParam().pre_mapping_fbv, GetParam().use_persistent_hash_map,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
 
     std::unique_ptr<DocumentStore> doc_store =
         std::move(create_result.document_store);
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                               doc_store->Put(test_document1_));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult put_result1,
+        doc_store->Put(document_util::CreateDocumentWrapper(test_document1_)));
     EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_result1.was_replacement());
+    EXPECT_FALSE(put_result1.was_replacement);
     document_id1 = put_result1.new_document_id;
 
     if (GetParam().use_persistent_hash_map) {
@@ -5295,7 +6842,9 @@
             /*use_persistent_hash_map=*/switch_key_mapper_flag,
             PortableFileBackedProtoLog<
                 DocumentWrapper>::kDefaultCompressionLevel,
-            &initialize_stats));
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel, &initialize_stats));
     EXPECT_THAT(initialize_stats.document_store_recovery_cause(),
                 Eq(InitializeStatsProto::IO_ERROR));
 
@@ -5332,21 +6881,25 @@
   {
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, document_store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              GetParam().pre_mapping_fbv,
-                              GetParam().use_persistent_hash_map,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, document_store_dir_, &fake_clock_,
+            schema_store_.get(), feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            GetParam().pre_mapping_fbv, GetParam().use_persistent_hash_map,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
 
     std::unique_ptr<DocumentStore> doc_store =
         std::move(create_result.document_store);
-    ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                               doc_store->Put(test_document1_));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        DocumentStore::PutResult put_result1,
+        doc_store->Put(document_util::CreateDocumentWrapper(test_document1_)));
     EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
-    EXPECT_FALSE(put_result1.was_replacement());
+    EXPECT_FALSE(put_result1.was_replacement);
     document_id1 = put_result1.new_document_id;
 
     if (GetParam().use_persistent_hash_map) {
@@ -5371,14 +6924,16 @@
     InitializeStatsProto initialize_stats;
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, document_store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              GetParam().pre_mapping_fbv,
-                              GetParam().use_persistent_hash_map,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              &initialize_stats));
+        DocumentStore::Create(
+            &filesystem_, document_store_dir_, &fake_clock_,
+            schema_store_.get(), feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            GetParam().pre_mapping_fbv, GetParam().use_persistent_hash_map,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel, &initialize_stats));
     EXPECT_THAT(initialize_stats.document_store_recovery_cause(),
                 Eq(InitializeStatsProto::NONE));
 
@@ -5406,11 +6961,7 @@
   }
 }
 
-TEST_P(DocumentStoreTest, GetDocumentIdByNamespaceIdFingerprint) {
-  std::string dynamic_trie_uri_mapper_dir =
-      document_store_dir_ + "/key_mapper_dir";
-  std::string persistent_hash_map_uri_mapper_dir =
-      document_store_dir_ + "/uri_mapper";
+TEST_P(DocumentStoreTest, GetDocumentId_expiredDocument) {
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       DocumentStore::Create(
@@ -5419,14 +6970,93 @@
           /*force_recovery_and_revalidate_documents=*/false,
           GetParam().pre_mapping_fbv, GetParam().use_persistent_hash_map,
           PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
+          PortableFileBackedProtoLog<
+              DocumentWrapper>::kDefaultCompressionThresholdBytes,
+          protobuf_ports::kDefaultMemLevel,
+          /*initialize_stats=*/nullptr));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  fake_clock_.SetSystemTimeMilliseconds(0);
+  DocumentProto foo_document = DocumentBuilder()
+                                   .SetCreationTimestampMs(0)
+                                   .SetTtlMs(1000)
+                                   .SetKey("namespace", "uri")
+                                   .SetSchema("email")
+                                   .SetCreationTimestampMs(0)
+                                   .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(foo_document)));
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result.was_replacement);
+  DocumentId document_id = put_result.new_document_id;
+
+  // Adjust the clock to make the document expired. GetDocumentId should still
+  // return the original document id.
+  fake_clock_.SetSystemTimeMilliseconds(2000);
+  EXPECT_THAT(doc_store->GetDocumentId("namespace", "uri"),
+              IsOkAndHolds(document_id));
+}
+
+TEST_P(DocumentStoreTest, GetDocumentId_deletedDocument) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      DocumentStore::Create(
+          &filesystem_, document_store_dir_, &fake_clock_, schema_store_.get(),
+          feature_flags_.get(),
+          /*force_recovery_and_revalidate_documents=*/false,
+          GetParam().pre_mapping_fbv, GetParam().use_persistent_hash_map,
+          PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
+          PortableFileBackedProtoLog<
+              DocumentWrapper>::kDefaultCompressionThresholdBytes,
+          protobuf_ports::kDefaultMemLevel,
+          /*initialize_stats=*/nullptr));
+  std::unique_ptr<DocumentStore> doc_store =
+      std::move(create_result.document_store);
+
+  DocumentProto foo_document = DocumentBuilder()
+                                   .SetKey("namespace", "uri")
+                                   .SetSchema("email")
+                                   .SetCreationTimestampMs(0)
+                                   .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(foo_document)));
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result.was_replacement);
+  DocumentId document_id = put_result.new_document_id;
+
+  // Delete the document. GetDocumentId should still return the original
+  // document id.
+  ICING_ASSERT_OK(doc_store->Delete(
+      document_id,
+      /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds()));
+  EXPECT_THAT(doc_store->GetDocumentId("namespace", "uri"),
+              IsOkAndHolds(document_id));
+}
+
+TEST_P(DocumentStoreTest, GetDocumentIdByNamespaceIdFingerprint) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      DocumentStore::Create(
+          &filesystem_, document_store_dir_, &fake_clock_, schema_store_.get(),
+          feature_flags_.get(),
+          /*force_recovery_and_revalidate_documents=*/false,
+          GetParam().pre_mapping_fbv, GetParam().use_persistent_hash_map,
+          PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
+          PortableFileBackedProtoLog<
+              DocumentWrapper>::kDefaultCompressionThresholdBytes,
+          protobuf_ports::kDefaultMemLevel,
           /*initialize_stats=*/nullptr));
 
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store->Put(test_document1_));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(test_document1_)));
   EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
-  EXPECT_FALSE(put_result.was_replacement());
+  EXPECT_FALSE(put_result.was_replacement);
   DocumentId document_id = put_result.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -5477,8 +7107,9 @@
                                .AddStringProperty("subject", "subject foo")
                                .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document)));
   DocumentId document_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentAssociatedScoreData score_data,
@@ -5503,8 +7134,9 @@
                                .AddStringProperty("subject", "subject foo")
                                .Build();
 
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store->Put(document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document)));
   DocumentId document_id = put_result.new_document_id;
   std::unique_ptr<ScorablePropertySet> scorable_property_set =
       doc_store->GetScorablePropertySet(
@@ -5579,11 +7211,13 @@
                                 .AddInt64Property("scoreInt64", 5)
                                 .SetCreationTimestampMs(0)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
 
   std::unique_ptr<ScorablePropertySet> scorable_property_set_doc1 =
@@ -5625,8 +7259,9 @@
                                 .Build();
   // Add document3 to the document store, it will result in document1 being
   // deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
-                             doc_store->Put(document3));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result3,
+      doc_store->Put(document_util::CreateDocumentWrapper(document3)));
   DocumentId document_id3 = put_result3.new_document_id;
   std::unique_ptr<ScorablePropertySet> scorable_property_set_doc3 =
       doc_store->GetScorablePropertySet(
@@ -5654,8 +7289,9 @@
                                 .Build();
   // Add document4 to the document store, it will result in document3 being
   // deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
-                             doc_store->Put(document4));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result4,
+      doc_store->Put(document_util::CreateDocumentWrapper(document4)));
   DocumentId document_id4 = put_result4.new_document_id;
   std::unique_ptr<ScorablePropertySet> scorable_property_set_doc4 =
       doc_store->GetScorablePropertySet(
@@ -5728,8 +7364,9 @@
                                 .AddDoubleProperty("scoreDouble", 1.5, 2.5)
                                 .SetCreationTimestampMs(0)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
-                             doc_store->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result1,
+      doc_store->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result1.new_document_id;
 
   std::unique_ptr<ScorablePropertySet> scorable_property_set_doc1 =
@@ -5760,8 +7397,9 @@
 
   // Add document2 to the document store, it will result in document1 being
   // deleted.
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
-                             doc_store->Put(document2));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      doc_store->Put(document_util::CreateDocumentWrapper(document2)));
   DocumentId document_id2 = put_result2.new_document_id;
   std::unique_ptr<ScorablePropertySet> scorable_property_set_doc2 =
       doc_store->GetScorablePropertySet(
@@ -5840,8 +7478,9 @@
                                 .SetCreationTimestampMs(1)
                                 .AddDoubleProperty("income", 10000, 20000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store->Put(document0));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document0)));
   DocumentId document_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentAssociatedScoreData score_data,
@@ -5914,8 +7553,9 @@
                                 .SetCreationTimestampMs(1)
                                 .AddDoubleProperty("income", 10000, 20000)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store->Put(document0));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document0)));
   DocumentId document_id = put_result.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentAssociatedScoreData score_data,
@@ -5999,10 +7639,13 @@
                                 .SetCreationTimestampMs(1)
                                 .AddDoubleProperty("score", 10, 20)
                                 .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
-                             doc_store->Put(document0));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document0)));
   DocumentId document_id0 = put_result.new_document_id;
-  ICING_ASSERT_OK_AND_ASSIGN(put_result, doc_store->Put(document1));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      put_result,
+      doc_store->Put(document_util::CreateDocumentWrapper(document1)));
   DocumentId document_id1 = put_result.new_document_id;
 
   // Update the schema by rearranging the schema types. Since SchemaTypeId is
@@ -6047,6 +7690,445 @@
       Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({10, 20}))));
 }
 
+TEST_P(DocumentStoreTest, GetResultGroupingEntryId_getByFilterName) {
+  // Put 2 schema types into the schema store.
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Email"))
+          .AddType(SchemaTypeConfigBuilder().SetType("Message"))
+          .Build();
+
+  std::string schema_store_dir = schema_store_dir_ + "_custom";
+  filesystem_.DeleteDirectoryRecursively(schema_store_dir.c_str());
+  filesystem_.CreateDirectoryRecursively(schema_store_dir.c_str());
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
+
+  ICING_ASSERT_OK(schema_store->SetSchema(
+      std::move(schema), /*ignore_errors_and_delete_documents=*/false));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store.get()));
+  std::unique_ptr<DocumentStore> document_store =
+      std::move(create_result.document_store);
+
+  // Put 3 documents into the document store to create 3 different namespaces.
+  DocumentProto document0 = DocumentBuilder()
+                                .SetKey("namespace0", "uri/0")
+                                .SetSchema("Email")
+                                .Build();
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("namespace1", "uri/1")
+                                .SetSchema("Message")
+                                .Build();
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("namespace2", "uri/2")
+                                .SetSchema("Message")
+                                .Build();
+  ICING_ASSERT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(std::move(document0))));
+  ICING_ASSERT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(std::move(document1))));
+  ICING_ASSERT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(std::move(document2))));
+
+  ASSERT_THAT(document_store->GetNamespaceId("namespace0"), IsOkAndHolds(0));
+  ASSERT_THAT(document_store->GetNamespaceId("namespace1"), IsOkAndHolds(1));
+  ASSERT_THAT(document_store->GetNamespaceId("namespace2"), IsOkAndHolds(2));
+
+  ASSERT_THAT(schema_store->GetSchemaTypeId("Email"), IsOkAndHolds(0));
+  ASSERT_THAT(schema_store->GetSchemaTypeId("Message"), IsOkAndHolds(1));
+
+  // NONE should always return std::nullopt.
+  EXPECT_THAT(
+      document_store->GetResultGroupingEntryId(
+          ResultSpecProto_ResultGroupingType_NONE, "namespace0", "Email"),
+      IsFalse());
+  EXPECT_THAT(
+      document_store->GetResultGroupingEntryId(
+          ResultSpecProto_ResultGroupingType_NONE, "namespace1", "Email"),
+      IsFalse());
+  EXPECT_THAT(
+      document_store->GetResultGroupingEntryId(
+          ResultSpecProto_ResultGroupingType_NONE, "namespace2", "Email"),
+      IsFalse());
+  EXPECT_THAT(
+      document_store->GetResultGroupingEntryId(
+          ResultSpecProto_ResultGroupingType_NONE, "namespace0", "Message"),
+      IsFalse());
+  EXPECT_THAT(
+      document_store->GetResultGroupingEntryId(
+          ResultSpecProto_ResultGroupingType_NONE, "namespace1", "Message"),
+      IsFalse());
+  EXPECT_THAT(
+      document_store->GetResultGroupingEntryId(
+          ResultSpecProto_ResultGroupingType_NONE, "namespace2", "Message"),
+      IsFalse());
+
+  // SCHEMA_TYPE should return id based on the schema type and ignore the
+  // namespace.
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_SCHEMA_TYPE, "namespace0",
+                  "Email"),
+              Optional(Eq(0)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_SCHEMA_TYPE, "namespace1",
+                  "Email"),
+              Optional(Eq(0)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_SCHEMA_TYPE, "namespace2",
+                  "Email"),
+              Optional(Eq(0)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_SCHEMA_TYPE, "namespace0",
+                  "Message"),
+              Optional(Eq(1)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_SCHEMA_TYPE, "namespace1",
+                  "Message"),
+              Optional(Eq(1)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_SCHEMA_TYPE, "namespace2",
+                  "Message"),
+              Optional(Eq(1)));
+
+  // NAMESPACE should return id based on the namespace and ignore the schema
+  // type.
+  EXPECT_THAT(
+      document_store->GetResultGroupingEntryId(
+          ResultSpecProto_ResultGroupingType_NAMESPACE, "namespace0", "Email"),
+      Optional(Eq(0)));
+  EXPECT_THAT(
+      document_store->GetResultGroupingEntryId(
+          ResultSpecProto_ResultGroupingType_NAMESPACE, "namespace1", "Email"),
+      Optional(Eq(1)));
+  EXPECT_THAT(
+      document_store->GetResultGroupingEntryId(
+          ResultSpecProto_ResultGroupingType_NAMESPACE, "namespace2", "Email"),
+      Optional(Eq(2)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE, "namespace0",
+                  "Message"),
+              Optional(Eq(0)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE, "namespace1",
+                  "Message"),
+              Optional(Eq(1)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE, "namespace2",
+                  "Message"),
+              Optional(Eq(2)));
+
+  // NAMESPACE_AND_SCHEMA_TYPE should return id based on both namespace and
+  // schema type.
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  "namespace0", "Email"),
+              Optional(Eq(0x00000000)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  "namespace1", "Email"),
+              Optional(Eq(0x00010000)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  "namespace2", "Email"),
+              Optional(Eq(0x00020000)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  "namespace0", "Message"),
+              Optional(Eq(0x00000001)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  "namespace1", "Message"),
+              Optional(Eq(0x00010001)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  "namespace2", "Message"),
+              Optional(Eq(0x00020001)));
+}
+
+TEST_P(DocumentStoreTest, GetResultGroupingEntryId_getByNonExistingFilterName) {
+  // Put 1 schema type into the schema store.
+  SchemaProto schema = SchemaBuilder()
+                           .AddType(SchemaTypeConfigBuilder().SetType("Email"))
+                           .Build();
+
+  std::string schema_store_dir = schema_store_dir_ + "_custom";
+  filesystem_.DeleteDirectoryRecursively(schema_store_dir.c_str());
+  filesystem_.CreateDirectoryRecursively(schema_store_dir.c_str());
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
+
+  ICING_ASSERT_OK(schema_store->SetSchema(
+      std::move(schema), /*ignore_errors_and_delete_documents=*/false));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store.get()));
+  std::unique_ptr<DocumentStore> document_store =
+      std::move(create_result.document_store);
+
+  // Put 1 document into the document store to create 1 namespace.
+  DocumentProto document0 = DocumentBuilder()
+                                .SetKey("namespace0", "uri/0")
+                                .SetSchema("Email")
+                                .Build();
+  ICING_ASSERT_OK(document_store->Put(
+      document_util::CreateDocumentWrapper(std::move(document0))));
+
+  ASSERT_THAT(document_store->GetNamespaceId("namespace0"), IsOkAndHolds(0));
+  ASSERT_THAT(schema_store->GetSchemaTypeId("Email"), IsOkAndHolds(0));
+
+  // NONE should always return std::nullopt.
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NONE,
+                  "nonExistingNamespace", "nonExistingSchemaType"),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NONE,
+                  "nonExistingNamespace", "Email"),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NONE, "namespace0",
+                  "nonExistingSchemaType"),
+              IsFalse());
+
+  // SCHEMA_TYPE should return id based on the schema type and ignore the
+  // namespace. It is ok that the namespace does not exist.
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_SCHEMA_TYPE,
+                  "nonExistingNamespace", "nonExistingSchemaType"),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_SCHEMA_TYPE,
+                  "nonExistingNamespace", "Email"),
+              Optional(Eq(0)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_SCHEMA_TYPE, "namespace0",
+                  "nonExistingSchemaType"),
+              IsFalse());
+
+  // NAMESPACE should return id based on the namespace and ignore the schema
+  // type. It is ok that the schema type does not exist.
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE,
+                  "nonExistingNamespace", "nonExistingSchemaType"),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE,
+                  "nonExistingNamespace", "Email"),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE, "namespace0",
+                  "nonExistingSchemaType"),
+              Optional(Eq(0)));
+
+  // NAMESPACE_AND_SCHEMA_TYPE should return id based on both namespace and
+  // schema type. Both namespace and schema type must exist.
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  "nonExistingNamespace", "nonExistingSchemaType"),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  "nonExistingNamespace", "Email"),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  "namespace0", "nonExistingSchemaType"),
+              IsFalse());
+}
+
+TEST_P(DocumentStoreTest, GetResultGroupingEntryId_getByIds) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> document_store =
+      std::move(create_result.document_store);
+
+  // GetResultGroupingEntryId() by id only handles the encoding and won't check
+  // if the id exists (except kInvalidNamespaceId and kInvalidSchemaTypeId), so
+  // we don't need to set up schema types and namespaces here.
+
+  // NONE should always return std::nullopt.
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NONE, /*namespace_id=*/0,
+                  /*schema_type_id=*/0),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NONE, /*namespace_id=*/0,
+                  /*schema_type_id=*/1),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NONE, /*namespace_id=*/1,
+                  /*schema_type_id=*/0),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NONE, /*namespace_id=*/1,
+                  /*schema_type_id=*/1),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NONE,
+                  /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
+                  /*schema_type_id=*/std::numeric_limits<SchemaTypeId>::max()),
+              IsFalse());
+
+  // SCHEMA_TYPE should return id based on the schema type id and ignore the
+  // namespace id.
+  EXPECT_THAT(
+      document_store->GetResultGroupingEntryId(
+          ResultSpecProto_ResultGroupingType_SCHEMA_TYPE, /*namespace_id=*/0,
+          /*schema_type_id=*/0),
+      Optional(Eq(0)));
+  EXPECT_THAT(
+      document_store->GetResultGroupingEntryId(
+          ResultSpecProto_ResultGroupingType_SCHEMA_TYPE, /*namespace_id=*/0,
+          /*schema_type_id=*/1),
+      Optional(Eq(1)));
+  EXPECT_THAT(
+      document_store->GetResultGroupingEntryId(
+          ResultSpecProto_ResultGroupingType_SCHEMA_TYPE, /*namespace_id=*/1,
+          /*schema_type_id=*/0),
+      Optional(Eq(0)));
+  EXPECT_THAT(
+      document_store->GetResultGroupingEntryId(
+          ResultSpecProto_ResultGroupingType_SCHEMA_TYPE, /*namespace_id=*/1,
+          /*schema_type_id=*/1),
+      Optional(Eq(1)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_SCHEMA_TYPE,
+                  /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
+                  /*schema_type_id=*/std::numeric_limits<SchemaTypeId>::max()),
+              Optional(Eq(std::numeric_limits<SchemaTypeId>::max())));
+
+  // NAMESPACE should return id based on the namespace id and ignore the schema
+  // type id.
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE,
+                  /*namespace_id=*/0, /*schema_type_id=*/0),
+              Optional(Eq(0)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE,
+                  /*namespace_id=*/0, /*schema_type_id=*/1),
+              Optional(Eq(0)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE,
+                  /*namespace_id=*/1, /*schema_type_id=*/0),
+              Optional(Eq(1)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE,
+                  /*namespace_id=*/1, /*schema_type_id=*/1),
+              Optional(Eq(1)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE,
+                  /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
+                  /*schema_type_id=*/std::numeric_limits<SchemaTypeId>::max()),
+              Optional(Eq(std::numeric_limits<NamespaceId>::max())));
+
+  // NAMESPACE_AND_SCHEMA_TYPE should return id based on both namespace and
+  // schema type ids.
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  /*namespace_id=*/0, /*schema_type_id=*/0),
+              Optional(Eq(0x00000000)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  /*namespace_id=*/0, /*schema_type_id=*/1),
+              Optional(Eq(0x00000001)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  /*namespace_id=*/1, /*schema_type_id=*/0),
+              Optional(Eq(0x00010000)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  /*namespace_id=*/1, /*schema_type_id=*/1),
+              Optional(Eq(0x00010001)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
+                  /*schema_type_id=*/std::numeric_limits<SchemaTypeId>::max()),
+              Optional(Eq(0x7fff7fff)));
+}
+
+TEST_P(DocumentStoreTest, GetResultGroupingEntryId_getByInvalidIds) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::CreateResult create_result,
+      CreateDocumentStore(&filesystem_, document_store_dir_, &fake_clock_,
+                          schema_store_.get()));
+  std::unique_ptr<DocumentStore> document_store =
+      std::move(create_result.document_store);
+
+  // GetResultGroupingEntryId() by id only handles the encoding and won't check
+  // if the id exists (except kInvalidNamespaceId and kInvalidSchemaTypeId), so
+  // we don't need to set up schema types and namespaces here.
+
+  // NONE should always return std::nullopt.
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NONE, kInvalidNamespaceId,
+                  kInvalidSchemaTypeId),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NONE, kInvalidNamespaceId,
+                  /*schema_type_id=*/0),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NONE, /*namespace_id=*/0,
+                  kInvalidSchemaTypeId),
+              IsFalse());
+
+  // SCHEMA_TYPE should return id based on the schema type id and ignore the
+  // namespace id. It is ok that the namespace id is invalid.
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_SCHEMA_TYPE,
+                  kInvalidNamespaceId, kInvalidSchemaTypeId),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_SCHEMA_TYPE,
+                  kInvalidNamespaceId, /*schema_type_id=*/0),
+              Optional(Eq(0)));
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_SCHEMA_TYPE,
+                  /*namespace_id=*/0, kInvalidSchemaTypeId),
+              IsFalse());
+
+  // NAMESPACE should return id based on the namespace id and ignore the schema
+  // type id. It is ok that the schema type id is invalid.
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE,
+                  kInvalidNamespaceId, kInvalidSchemaTypeId),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE,
+                  kInvalidNamespaceId, /*schema_type_id=*/0),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE,
+                  /*namespace_id=*/0, kInvalidSchemaTypeId),
+              Optional(Eq(0)));
+  // NAMESPACE_AND_SCHEMA_TYPE should return id based on both namespace and
+  // schema type ids. Both namespace and schema type ids must be valid.
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  kInvalidNamespaceId, kInvalidSchemaTypeId),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  kInvalidNamespaceId, /*schema_type_id=*/0),
+              IsFalse());
+  EXPECT_THAT(document_store->GetResultGroupingEntryId(
+                  ResultSpecProto_ResultGroupingType_NAMESPACE_AND_SCHEMA_TYPE,
+                  /*namespace_id=*/0, kInvalidSchemaTypeId),
+              IsFalse());
+}
+
 INSTANTIATE_TEST_SUITE_P(
     DocumentStoreTest, DocumentStoreTest,
     testing::Values(
diff --git a/icing/store/dynamic-trie-key-mapper.h b/icing/store/dynamic-trie-key-mapper.h
index f7fd665..e12b44e 100644
--- a/icing/store/dynamic-trie-key-mapper.h
+++ b/icing/store/dynamic-trie-key-mapper.h
@@ -81,7 +81,7 @@
 
   libtextclassifier3::StatusOr<T> Get(std::string_view key) const override;
 
-  bool Delete(std::string_view key) override;
+  libtextclassifier3::Status Delete(std::string_view key) override;
 
   std::unique_ptr<typename KeyMapper<T, Formatter>::Iterator> GetIterator()
       const override;
@@ -277,7 +277,8 @@
 }
 
 template <typename T, typename Formatter>
-bool DynamicTrieKeyMapper<T, Formatter>::Delete(std::string_view key) {
+libtextclassifier3::Status DynamicTrieKeyMapper<T, Formatter>::Delete(
+    std::string_view key) {
   return trie_.Delete(key);
 }
 
diff --git a/icing/store/key-mapper.h b/icing/store/key-mapper.h
index 83f725c..c7d5bb0 100644
--- a/icing/store/key-mapper.h
+++ b/icing/store/key-mapper.h
@@ -80,8 +80,8 @@
   // Returns any encountered IO errors.
   virtual libtextclassifier3::StatusOr<T> Get(std::string_view key) const = 0;
 
-  // Deletes data related to the given key. Returns true on success.
-  virtual bool Delete(std::string_view key) = 0;
+  // Deletes data related to the given key. Returns OK on success.
+  virtual libtextclassifier3::Status Delete(std::string_view key) = 0;
 
   // Returns an iterator of the key mapper.
   //
diff --git a/icing/store/key-mapper_test.cc b/icing/store/key-mapper_test.cc
index 229e519..f75fc37 100644
--- a/icing/store/key-mapper_test.cc
+++ b/icing/store/key-mapper_test.cc
@@ -235,7 +235,7 @@
               UnorderedElementsAre(Pair("foo", 1)));
 
   // Delete "foo".
-  EXPECT_THAT(key_mapper->Delete("foo"), IsTrue());
+  ICING_EXPECT_OK(key_mapper->Delete("foo"));
 
   // Verify num_keys().
   EXPECT_THAT(key_mapper->num_keys(), 0);
@@ -269,7 +269,7 @@
       UnorderedElementsAre(Pair("foo", 1), Pair("bar", 2), Pair("baz", 3)));
 
   // Delete "foo".
-  EXPECT_THAT(key_mapper->Delete("foo"), IsTrue());
+  ICING_EXPECT_OK(key_mapper->Delete("foo"));
 
   // Verify num_keys().
   EXPECT_THAT(key_mapper->num_keys(), 2);
@@ -310,9 +310,9 @@
       UnorderedElementsAre(Pair("foo", 1), Pair("bar", 2), Pair("baz", 3)));
 
   // Delete all keys.
-  EXPECT_THAT(key_mapper->Delete("foo"), IsTrue());
-  EXPECT_THAT(key_mapper->Delete("bar"), IsTrue());
-  EXPECT_THAT(key_mapper->Delete("baz"), IsTrue());
+  ICING_EXPECT_OK(key_mapper->Delete("foo"));
+  ICING_EXPECT_OK(key_mapper->Delete("bar"));
+  ICING_EXPECT_OK(key_mapper->Delete("baz"));
 
   // Verify num_keys().
   EXPECT_THAT(key_mapper->num_keys(), 0);
diff --git a/icing/store/persistent-hash-map-key-mapper.h b/icing/store/persistent-hash-map-key-mapper.h
index e8b5832..21381e4 100644
--- a/icing/store/persistent-hash-map-key-mapper.h
+++ b/icing/store/persistent-hash-map-key-mapper.h
@@ -108,8 +108,8 @@
     return value;
   }
 
-  bool Delete(std::string_view key) override {
-    return persistent_hash_map_->Delete(key).ok();
+  libtextclassifier3::Status Delete(std::string_view key) override {
+    return persistent_hash_map_->Delete(key);
   }
 
   std::unique_ptr<typename KeyMapper<T, Formatter>::Iterator> GetIterator()
diff --git a/icing/testing/common-matchers.h b/icing/testing/common-matchers.h
index d730c83..8e94a7c 100644
--- a/icing/testing/common-matchers.h
+++ b/icing/testing/common-matchers.h
@@ -93,27 +93,47 @@
 }
 
 // Used to match a DocHitInfoIterator::CallStats
-MATCHER_P5(EqualsDocHitInfoIteratorCallStats, num_leaf_advance_calls_lite_index,
+MATCHER_P6(EqualsDocHitInfoIteratorCallStats, num_leaf_advance_calls_lite_index,
            num_leaf_advance_calls_main_index,
            num_leaf_advance_calls_integer_index,
-           num_leaf_advance_calls_no_index, num_blocks_inspected, "") {
+           num_leaf_advance_calls_no_index, num_blocks_inspected,
+           embedding_stats, "") {
   const DocHitInfoIterator::CallStats& actual = arg;
+  auto embedding_stats_to_string =
+      [](const DocHitInfoIterator::CallStats::EmbeddingStats& stats) {
+        return absl_ports::StrCat(
+            "{num_unquantized_embeddings_scored=",
+            std::to_string(stats.num_unquantized_embeddings_scored),
+            ", num_quantized_embeddings_scored=",
+            std::to_string(stats.num_quantized_embeddings_scored),
+            ", unquantized_shards_read=[",
+            absl_ports::StrJoin(stats.unquantized_shards_read, ",",
+                                absl_ports::NumberFormatter()),
+            "], quantized_shards_read=[",
+            absl_ports::StrJoin(stats.quantized_shards_read, ",",
+                                absl_ports::NumberFormatter()),
+            "], num_embedding_bytes_read=",
+            std::to_string(stats.num_embedding_bytes_read), "}");
+      };
   *result_listener << IcingStringUtil::StringPrintf(
       "(actual is {num_leaf_advance_calls_lite_index=%d, "
       "num_leaf_advance_calls_main_index=%d, "
       "num_leaf_advance_calls_integer_index=%d, "
-      "num_leaf_advance_calls_no_index=%d, num_blocks_inspected=%d}, but "
+      "num_leaf_advance_calls_no_index=%d, num_blocks_inspected=%d, "
+      "embedding_stats=%s}, but "
       "expected was {num_leaf_advance_calls_lite_index=%d, "
       "num_leaf_advance_calls_main_index=%d, "
       "num_leaf_advance_calls_integer_index=%d, "
-      "num_leaf_advance_calls_no_index=%d, num_blocks_inspected=%d}.)",
+      "num_leaf_advance_calls_no_index=%d, num_blocks_inspected=%d, "
+      "embedding_stats=%s}.)",
       actual.num_leaf_advance_calls_lite_index,
       actual.num_leaf_advance_calls_main_index,
       actual.num_leaf_advance_calls_integer_index,
       actual.num_leaf_advance_calls_no_index, actual.num_blocks_inspected,
+      embedding_stats_to_string(actual.embedding_stats).c_str(),
       num_leaf_advance_calls_lite_index, num_leaf_advance_calls_main_index,
       num_leaf_advance_calls_integer_index, num_leaf_advance_calls_no_index,
-      num_blocks_inspected);
+      num_blocks_inspected, embedding_stats_to_string(embedding_stats).c_str());
   return actual.num_leaf_advance_calls_lite_index ==
              num_leaf_advance_calls_lite_index &&
          actual.num_leaf_advance_calls_main_index ==
@@ -122,7 +142,8 @@
              num_leaf_advance_calls_integer_index &&
          actual.num_leaf_advance_calls_no_index ==
              num_leaf_advance_calls_no_index &&
-         actual.num_blocks_inspected == num_blocks_inspected;
+         actual.num_blocks_inspected == num_blocks_inspected &&
+         actual.embedding_stats == embedding_stats;
 }
 
 // Used to match a DocumentAssociatedScoreData
@@ -139,6 +160,13 @@
              has_valid_scorable_property_cache_index;
 }
 
+MATCHER_P4(EqualsDocumentMetadata, schema_type_name, name_space, uri,
+           document_id, "") {
+  return arg.schema_type_name == schema_type_name &&
+         arg.name_space == name_space && arg.uri == uri &&
+         arg.document_id == document_id;
+}
+
 // Used to match a ScorablePropertyManager::ScorablePropertyInfo
 MATCHER_P2(EqualsScorablePropertyInfo, property_path, data_type, "") {
   const ScorablePropertyManager::ScorablePropertyInfo& actual = arg;
@@ -612,6 +640,7 @@
   SearchResultProto actual_copy = arg;
   actual_copy.clear_query_stats();
   actual_copy.clear_debug_info();
+  actual_copy.clear_vm_binder_transaction_latency_start_time_ms();
   for (SearchResultProto::ResultProto& result :
        *actual_copy.mutable_results()) {
     // Joined results
@@ -625,6 +654,7 @@
   SearchResultProto expected_copy = expected;
   expected_copy.clear_query_stats();
   expected_copy.clear_debug_info();
+  actual_copy.clear_vm_binder_transaction_latency_start_time_ms();
   for (SearchResultProto::ResultProto& result :
        *expected_copy.mutable_results()) {
     // Joined results
diff --git a/icing/testing/embedding-test-utils.cc b/icing/testing/embedding-test-utils.cc
index 23f4197..db2d622 100644
--- a/icing/testing/embedding-test-utils.cc
+++ b/icing/testing/embedding-test-utils.cc
@@ -24,9 +24,10 @@
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/index/embed/embedding-hit.h"
 #include "icing/index/embed/embedding-index.h"
-#include "icing/index/embed/posting-list-embedding-hit-accessor.h"
+#include "icing/index/embed/embedding-query-results.h"
 #include "icing/index/embed/quantizer.h"
 #include "icing/proto/document.pb.h"
+#include "icing/store/document-id.h"
 #include "icing/util/status-macros.h"
 
 namespace icing {
@@ -38,18 +39,20 @@
                           std::string_view model_signature) {
   std::vector<EmbeddingHit> hits;
 
-  libtextclassifier3::StatusOr<std::unique_ptr<PostingListEmbeddingHitAccessor>>
-      pl_accessor_or = embedding_index->GetAccessor(dimension, model_signature);
-  if (absl_ports::IsNotFound(pl_accessor_or.status())) {
+  libtextclassifier3::StatusOr<
+      std::unique_ptr<EmbeddingIndex::EmbeddingHitAccessor>>
+      embedding_hit_accessor_or =
+          embedding_index->GetAccessor(dimension, model_signature);
+  if (absl_ports::IsNotFound(embedding_hit_accessor_or.status())) {
     return hits;
   }
-  ICING_ASSIGN_OR_RETURN(
-      std::unique_ptr<PostingListEmbeddingHitAccessor> pl_accessor,
-      std::move(pl_accessor_or));
+  ICING_ASSIGN_OR_RETURN(std::unique_ptr<EmbeddingIndex::EmbeddingHitAccessor>
+                             embedding_hit_accessor,
+                         std::move(embedding_hit_accessor_or));
 
   while (true) {
     ICING_ASSIGN_OR_RETURN(std::vector<EmbeddingHit> batch,
-                           pl_accessor->GetNextHitsBatch());
+                           embedding_hit_accessor->GetNextHitsBatch());
     if (batch.empty()) {
       return hits;
     }
@@ -58,20 +61,24 @@
 }
 
 std::vector<float> GetRawEmbeddingDataFromIndex(
-    const EmbeddingIndex* embedding_index) {
+    const EmbeddingIndex* embedding_index, uint32_t shard_id) {
   ICING_ASSIGN_OR_RETURN(const float* data,
-                         embedding_index->GetRawEmbeddingData(),
+                         embedding_index->GetRawEmbeddingData(shard_id),
                          std::vector<float>());
-  return std::vector<float>(data, data + embedding_index->GetTotalVectorSize());
+  return std::vector<float>(
+      data, data + embedding_index->GetTotalVectorSize(shard_id));
 }
 
 libtextclassifier3::StatusOr<std::vector<float>>
 GetAndRestoreQuantizedEmbeddingVectorFromIndex(
     const EmbeddingIndex* embedding_index, const EmbeddingHit& hit,
-    uint32_t dimension) {
+    uint32_t dimension, std::string_view model_signature,
+    std::string_view schema_name) {
+  uint32_t shard_id =
+      embedding_index->GetShardId(dimension, model_signature, schema_name);
   ICING_ASSIGN_OR_RETURN(
       const char* data,
-      embedding_index->GetQuantizedEmbeddingVector(hit, dimension));
+      embedding_index->GetQuantizedEmbeddingVector(hit, dimension, shard_id));
   Quantizer quantizer(data);
   const uint8_t* quantized_vector =
       reinterpret_cast<const uint8_t*>(data + sizeof(Quantizer));
@@ -83,5 +90,16 @@
   return result;
 }
 
+EmbeddingMatchInfos& GetOrCreateEmbeddingMatchInfosForDocument(
+    EmbeddingQueryResults& embedding_query_results, int query_vector_index,
+    SearchSpecProto::EmbeddingQueryMetricType::Code metric_type,
+    DocumentId doc_id) {
+  EmbeddingQueryResults::EmbeddingQueryMatchInfoMap* info_map =
+      embedding_query_results
+          .GetOrCreateMatchInfoMap(query_vector_index, metric_type)
+          .ValueOrDie();
+  return (*info_map)[doc_id];
+}
+
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/testing/embedding-test-utils.h b/icing/testing/embedding-test-utils.h
index 7f0b0c1..1908561 100644
--- a/icing/testing/embedding-test-utils.h
+++ b/icing/testing/embedding-test-utils.h
@@ -24,7 +24,9 @@
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/index/embed/embedding-hit.h"
 #include "icing/index/embed/embedding-index.h"
+#include "icing/index/embed/embedding-query-results.h"
 #include "icing/proto/document.pb.h"
+#include "icing/store/document-id.h"
 
 namespace icing {
 namespace lib {
@@ -50,14 +52,22 @@
                           uint32_t dimension, std::string_view model_signature);
 
 std::vector<float> GetRawEmbeddingDataFromIndex(
-    const EmbeddingIndex* embedding_index);
+    const EmbeddingIndex* embedding_index, uint32_t shard_id);
 
 // Gets the quantized embedding vector from the index based on the given hit,
 // and returns the dequantized version of the vector.
 libtextclassifier3::StatusOr<std::vector<float>>
 GetAndRestoreQuantizedEmbeddingVectorFromIndex(
     const EmbeddingIndex* embedding_index, const EmbeddingHit& hit,
-    uint32_t dimension);
+    uint32_t dimension, std::string_view model_signature,
+    std::string_view schema_name);
+
+// Gets or creates the EmbeddingMatchInfos in embedding_query_results for the
+// given query_vector_index, metric_type, and document.
+EmbeddingMatchInfos& GetOrCreateEmbeddingMatchInfosForDocument(
+    EmbeddingQueryResults& embedding_query_results, int query_vector_index,
+    SearchSpecProto::EmbeddingQueryMetricType::Code metric_type,
+    DocumentId doc_id);
 
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/testing/fake-clock.h b/icing/testing/fake-clock.h
index f451753..a7783e9 100644
--- a/icing/testing/fake-clock.h
+++ b/icing/testing/fake-clock.h
@@ -15,6 +15,10 @@
 #ifndef ICING_TESTING_FAKE_CLOCK_H_
 #define ICING_TESTING_FAKE_CLOCK_H_
 
+#include <atomic>
+#include <cstdint>
+#include <memory>
+
 #include "icing/util/clock.h"
 
 namespace icing {
@@ -55,7 +59,7 @@
   }
 
  private:
-  int64_t milliseconds_ = 0;
+  std::atomic<int64_t> milliseconds_ = 0;
   FakeTimer fake_timer_;
 };
 
diff --git a/icing/testing/test-feature-flags.cc b/icing/testing/test-feature-flags.cc
index 3d167b4..416cf9c 100644
--- a/icing/testing/test-feature-flags.cc
+++ b/icing/testing/test-feature-flags.cc
@@ -20,13 +20,28 @@
 namespace lib {
 
 FeatureFlags GetTestFeatureFlags() {
-  return FeatureFlags(/*enable_circular_schema_definitions=*/true,
-                      /*enable_scorable_properties=*/true,
-                      /*enable_embedding_quantization=*/true,
-                      /*enable_repeated_field_joins=*/true,
-                      /*enable_embedding_backup_generation=*/true,
-                      /*enable_schema_database=*/true,
-                      /*release_backup_schema_file_if_overlay_present=*/true);
+  return FeatureFlags(
+      /*enable_circular_schema_definitions=*/true,
+      /*enable_scorable_properties=*/true,
+      /*enable_embedding_quantization=*/true,
+      /*enable_repeated_field_joins=*/true,
+      /*enable_embedding_backup_generation=*/true,
+      /*enable_schema_database=*/true,
+      /*release_backup_schema_file_if_overlay_present=*/true,
+      /*enable_strict_page_byte_size_limit=*/true,
+      /*enable_smaller_decompression_buffer_size=*/true,
+      /*enable_eigen_embedding_scoring=*/true,
+      /*enable_passing_filter_to_children=*/true,
+      /*enable_proto_log_new_header_format=*/true,
+      /*enable_embedding_iterator_v2=*/true,
+      /*enable_reusable_decompression_buffer=*/true,
+      /*enable_schema_type_id_optimization=*/true,
+      /*enable_optimize_improvements=*/true,
+      /*expired_document_purge_threshold_ms=*/0,
+      /*enable_non_existent_qualified_id_join=*/true,
+      /*enable_skip_set_schema_type_equality_check=*/true,
+      /*enable_embed_query_optimization=*/true,
+      /*enable_schema_definition_deduping=*/true);
 }
 
 }  // namespace lib
diff --git a/icing/text_classifier/lib3/utils/base/config.h b/icing/text_classifier/lib3/utils/base/config.h
deleted file mode 100644
index 88af90b..0000000
--- a/icing/text_classifier/lib3/utils/base/config.h
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2019 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-// Define macros to indicate C++ standard / platform / etc we use.
-
-#ifndef ICING_TEXT_CLASSIFIER_LIB3_UTILS_BASE_CONFIG_H_
-#define ICING_TEXT_CLASSIFIER_LIB3_UTILS_BASE_CONFIG_H_
-
-namespace libtextclassifier3 {
-
-// Define LANG_CXX11 to 1 if current compiler supports C++11.
-//
-// GXX_EXPERIMENTAL_CXX0X is defined by gcc and clang up to at least
-// gcc-4.7 and clang-3.1 (2011-12-13).  __cplusplus was defined to 1
-// in gcc before 4.7 (Crosstool 16) and clang before 3.1, but is
-// defined according to the language version in effect thereafter.
-// Microsoft Visual Studio 14 (2015) sets __cplusplus==199711 despite
-// reasonably good C++11 support, so we set LANG_CXX for it and
-// newer versions (_MSC_VER >= 1900).
-#if (defined(__GXX_EXPERIMENTAL_CXX0X__) || __cplusplus >= 201103L || \
-     (defined(_MSC_VER) && _MSC_VER >= 1900))
-// Define this to 1 if the code is compiled in C++11 mode; leave it
-// undefined otherwise.  Do NOT define it to 0 -- that causes
-// '#ifdef LANG_CXX11' to behave differently from '#if LANG_CXX11'.
-#define LANG_CXX11 1
-#endif
-
-}  // namespace libtextclassifier3
-
-#endif  // ICING_TEXT_CLASSIFIER_LIB3_UTILS_BASE_CONFIG_H_
diff --git a/icing/text_classifier/lib3/utils/base/integral_types.h b/icing/text_classifier/lib3/utils/base/integral_types.h
index 3baa903..242c913 100644
--- a/icing/text_classifier/lib3/utils/base/integral_types.h
+++ b/icing/text_classifier/lib3/utils/base/integral_types.h
@@ -17,7 +17,6 @@
 #ifndef ICING_TEXT_CLASSIFIER_LIB3_UTILS_BASE_INTEGRAL_TYPES_H_
 #define ICING_TEXT_CLASSIFIER_LIB3_UTILS_BASE_INTEGRAL_TYPES_H_
 
-#include "icing/text_classifier/lib3/utils/base/config.h"
 
 namespace libtextclassifier3 {
 
@@ -46,8 +45,6 @@
 #endif  // COMPILER_MSVC
 
 // Some compile-time assertions that our new types have the intended size.
-// static_assert exists only since C++11, so we need an ifdef.
-#ifdef LANG_CXX11
 static_assert(sizeof(int) == 4, "Our typedefs depend on int being 32 bits");
 static_assert(sizeof(uint32) == 4, "wrong size");
 static_assert(sizeof(int32) == 4, "wrong size");
@@ -57,7 +54,6 @@
 static_assert(sizeof(int16) == 2, "wrong size");
 static_assert(sizeof(char32) == 4, "wrong size");
 static_assert(sizeof(int64) == 8, "wrong size");
-#endif  // LANG_CXX11
 
 // There are still some requirements that we build these headers in
 // C-compatibility mode. Unfortunately, -Wall doesn't like c-style
diff --git a/icing/text_classifier/lib3/utils/base/macros.h b/icing/text_classifier/lib3/utils/base/macros.h
index f6642d2..eefaeda 100644
--- a/icing/text_classifier/lib3/utils/base/macros.h
+++ b/icing/text_classifier/lib3/utils/base/macros.h
@@ -15,26 +15,15 @@
 #ifndef ICING_TEXT_CLASSIFIER_LIB3_UTILS_BASE_MACROS_H_
 #define ICING_TEXT_CLASSIFIER_LIB3_UTILS_BASE_MACROS_H_
 
-#include "icing/text_classifier/lib3/utils/base/config.h"
 
 namespace libtextclassifier3 {
 
 #define TC3_ARRAYSIZE(a) \
   ((sizeof(a) / sizeof(*(a))) / (size_t)(!(sizeof(a) % sizeof(*(a)))))
 
-#if LANG_CXX11
 #define TC3_DISALLOW_COPY_AND_ASSIGN(TypeName) \
   TypeName(const TypeName &) = delete;         \
   TypeName &operator=(const TypeName &) = delete
-#else  // C++98 case follows
-
-// Note that these C++98 implementations cannot completely disallow copying,
-// as members and friends can still accidentally make elided copies without
-// triggering a linker error.
-#define TC3_DISALLOW_COPY_AND_ASSIGN(TypeName) \
-  TypeName(const TypeName &);                  \
-  TypeName &operator=(const TypeName &)
-#endif  // LANG_CXX11
 
 // The TC3_FALLTHROUGH_INTENDED macro can be used to annotate implicit
 // fall-through between switch labels:
diff --git a/icing/text_classifier/lib3/utils/hash/farmhash.cc b/icing/text_classifier/lib3/utils/hash/farmhash.cc
index c04df07..10da72a 100644
--- a/icing/text_classifier/lib3/utils/hash/farmhash.cc
+++ b/icing/text_classifier/lib3/utils/hash/farmhash.cc
@@ -29,13 +29,6 @@
 #define FARMHASH_ASSUME_AESNI 1
 #endif
 
-#if !defined(FARMHASH_CAN_USE_CXX11) && defined(LANG_CXX11)
-#define FARMHASH_CAN_USE_CXX11 1
-#else
-#undef FARMHASH_CAN_USE_CXX11
-#define FARMHASH_CAN_USE_CXX11 0
-#endif
-
 // FARMHASH PORTABILITY LAYER: Runtime error if misconfigured
 
 #ifndef FARMHASH_DIE_IF_MISCONFIGURED
@@ -284,11 +277,6 @@
 #endif
 // Building blocks for hash functions
 
-// std::swap() was in <algorithm> but is in <utility> from C++11 on.
-#if !FARMHASH_CAN_USE_CXX11
-#include <algorithm>
-#endif
-
 #undef PERMUTE3
 #define PERMUTE3(a, b, c) do { std::swap(a, b); std::swap(a, c); } while (0)
 
diff --git a/icing/tokenization/combined-tokenizer_test.cc b/icing/tokenization/combined-tokenizer_test.cc
index 520ea47..b769e28 100644
--- a/icing/tokenization/combined-tokenizer_test.cc
+++ b/icing/tokenization/combined-tokenizer_test.cc
@@ -34,6 +34,7 @@
 #include "icing/index/numeric/numeric-index.h"
 #include "icing/jni/jni-cache.h"
 #include "icing/legacy/index/icing-filesystem.h"
+#include "icing/portable/gzip_stream.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/query/query-processor.h"
@@ -99,14 +100,18 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(&filesystem_, store_dir_, &fake_clock_,
-                              schema_store_.get(), feature_flags_.get(),
-                              /*force_recovery_and_revalidate_documents=*/false,
-                              /*pre_mapping_fbv=*/false,
-                              /*use_persistent_hash_map=*/false,
-                              PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDefaultCompressionLevel,
-                              /*initialize_stats=*/nullptr));
+        DocumentStore::Create(
+            &filesystem_, store_dir_, &fake_clock_, schema_store_.get(),
+            feature_flags_.get(),
+            /*force_recovery_and_revalidate_documents=*/false,
+            /*pre_mapping_fbv=*/false,
+            /*use_persistent_hash_map=*/false,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionLevel,
+            PortableFileBackedProtoLog<
+                DocumentWrapper>::kDefaultCompressionThresholdBytes,
+            protobuf_ports::kDefaultMemLevel,
+            /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
 
     Index::Options options(index_dir_,
@@ -114,7 +119,8 @@
                            /*lite_index_sort_at_indexing=*/true,
                            /*lite_index_sort_size=*/1024 * 8);
     ICING_ASSERT_OK_AND_ASSIGN(
-        index_, Index::Create(options, &filesystem_, &icing_filesystem_));
+        index_, Index::Create(options, &filesystem_, &icing_filesystem_,
+                              feature_flags_.get()));
     // TODO(b/249829533): switch to use persistent numeric index.
     ICING_ASSERT_OK_AND_ASSIGN(
         numeric_index_,
@@ -122,7 +128,8 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         embedding_index_,
         EmbeddingIndex::Create(&filesystem_, embedding_index_dir_, &fake_clock_,
-                               feature_flags_.get()));
+                               feature_flags_.get(),
+                               /*num_shards=*/32));
 
     language_segmenter_factory::SegmenterOptions segmenter_options(
         ULOC_US, jni_cache_.get());
diff --git a/icing/tokenization/icu/icu-language-segmenter-factory.cc b/icing/tokenization/icu/icu-language-segmenter-factory.cc
index 4f8263b..4117733 100644
--- a/icing/tokenization/icu/icu-language-segmenter-factory.cc
+++ b/icing/tokenization/icu/icu-language-segmenter-factory.cc
@@ -12,9 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include <utility>
+
 #include "icing/tokenization/icu/icu-language-segmenter.h"
 #include "icing/tokenization/language-segmenter-factory.h"
 #include "icing/util/logging.h"
+#include "icing/util/status-util.h"
 #include "unicode/uloc.h"
 
 namespace icing {
@@ -22,11 +25,17 @@
 
 namespace language_segmenter_factory {
 
+using ::icing::lib::status_util::TransformStatus;
+
 namespace {
 constexpr std::string_view kLocaleAmericanEnglishComputer = "en_US_POSIX";
 }  // namespace
 
-// Creates a language segmenter with the given locale.
+// Creates a language segmenter based on the provided options.
+//
+// @param options: The options for creating the language segmenter.
+// @param icu_segmenter_creation_status: Optional output parameter that will be
+//        populated with the status of IcuLanguageSegmenter.
 //
 // Returns:
 //   A LanguageSegmenter on success
@@ -36,7 +45,7 @@
 // users. Right now illegal locale strings will be ignored by ICU. ICU
 // components will be created with its default locale.
 libtextclassifier3::StatusOr<std::unique_ptr<LanguageSegmenter>> Create(
-    SegmenterOptions options) {
+    SegmenterOptions options, StatusProto* icu_segmenter_creation_status) {
   // Word connector rules for "en_US_POSIX" (American English (Computer)) are
   // different from other locales. E.g. "email.subject" will be split into 3
   // terms in "en_US_POSIX": "email", ".", and "subject", while it's just one
@@ -47,7 +56,12 @@
                        << " not supported. Converting to locale " << ULOC_US;
     options.locale = ULOC_US;
   }
-  return IcuLanguageSegmenter::Create(std::move(options.locale));
+  auto icu_segmenter_or =
+      IcuLanguageSegmenter::Create(std::move(options.locale));
+  if (icu_segmenter_creation_status != nullptr) {
+    TransformStatus(icu_segmenter_or.status(), icu_segmenter_creation_status);
+  }
+  return icu_segmenter_or;
 }
 
 }  // namespace language_segmenter_factory
diff --git a/icing/tokenization/icu/icu-language-segmenter_test.cc b/icing/tokenization/icu/icu-language-segmenter_test.cc
index cde62f2..f1c8f1a 100644
--- a/icing/tokenization/icu/icu-language-segmenter_test.cc
+++ b/icing/tokenization/icu/icu-language-segmenter_test.cc
@@ -61,6 +61,16 @@
 
 }  // namespace
 
+TEST_P(IcuLanguageSegmenterAllLocalesTest, SuccessfulIcuSegmenterSetsOkStatus) {
+  StatusProto icu_segmenter_creation_status;
+  ICING_ASSERT_OK_AND_ASSIGN(auto language_segmenter,
+                             language_segmenter_factory::Create(
+                                 language_segmenter_factory::SegmenterOptions(
+                                     GetLocale(), jni_cache_.get()),
+                                 &icu_segmenter_creation_status));
+  EXPECT_THAT(icu_segmenter_creation_status.code(), Eq(StatusProto::OK));
+}
+
 TEST_P(IcuLanguageSegmenterAllLocalesTest, EmptyText) {
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
diff --git a/icing/tokenization/language-segmenter-factory.h b/icing/tokenization/language-segmenter-factory.h
index 5e4879c..24d8930 100644
--- a/icing/tokenization/language-segmenter-factory.h
+++ b/icing/tokenization/language-segmenter-factory.h
@@ -21,6 +21,7 @@
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/jni/jni-cache.h"
+#include "icing/proto/status.pb.h"
 #include "icing/tokenization/language-segmenter.h"
 
 namespace icing {
@@ -53,13 +54,19 @@
   bool enable_icu_segmenter;
 };
 
-// Creates a language segmenter with the given locale.
+// Creates a language segmenter based on the provided options.
+//
+// @param options: The options for creating the language segmenter.
+// @param icu_segmenter_creation_status: Optional output parameter that will be
+//        populated with the status of IcuLanguageSegmenter if it's creation is
+//        attempted.
 //
 // Returns:
 //   A LanguageSegmenter on success
 //   INVALID_ARGUMENT_ERROR if locale string is invalid
 libtextclassifier3::StatusOr<std::unique_ptr<LanguageSegmenter>> Create(
-    SegmenterOptions options);
+    SegmenterOptions options,
+    StatusProto* icu_segmenter_creation_status = nullptr);
 
 }  // namespace language_segmenter_factory
 
diff --git a/icing/tokenization/plain-tokenizer.cc b/icing/tokenization/plain-tokenizer.cc
index ec7a783..c67b289 100644
--- a/icing/tokenization/plain-tokenizer.cc
+++ b/icing/tokenization/plain-tokenizer.cc
@@ -20,6 +20,7 @@
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/tokenization/language-segmenter.h"
+#include "icing/tokenization/token.h"
 #include "icing/util/character-iterator.h"
 #include "icing/util/i18n-utils.h"
 #include "icing/util/status-macros.h"
@@ -67,12 +68,11 @@
     return found_next_valid_term;
   }
 
-  std::vector<Token> GetTokens() const override {
-    std::vector<Token> result;
+  void GetTokens(std::vector<Token>* out_tokens) const override {
+    out_tokens->clear();
     if (!current_term_.empty()) {
-      result.push_back(Token(Token::Type::REGULAR, current_term_));
+      out_tokens->emplace_back(Token::Type::REGULAR, current_term_);
     }
-    return result;
   }
 
   libtextclassifier3::StatusOr<CharacterIterator> CalculateTokenStart()
@@ -143,8 +143,9 @@
   ICING_ASSIGN_OR_RETURN(std::unique_ptr<Tokenizer::Iterator> iterator,
                          Tokenize(text));
   std::vector<Token> tokens;
+  std::vector<Token> batch_tokens;
   while (iterator->Advance()) {
-    std::vector<Token> batch_tokens = iterator->GetTokens();
+    iterator->GetTokens(&batch_tokens);
     tokens.insert(tokens.end(), batch_tokens.begin(), batch_tokens.end());
   }
   return tokens;
diff --git a/icing/tokenization/plain-tokenizer_test.cc b/icing/tokenization/plain-tokenizer_test.cc
index 08d7f3e..20f74c8 100644
--- a/icing/tokenization/plain-tokenizer_test.cc
+++ b/icing/tokenization/plain-tokenizer_test.cc
@@ -72,7 +72,7 @@
                              plain_tokenizer->Tokenize(kText));
 
   // We should get no tokens if we get the token before advancing.
-  EXPECT_THAT(token_iterator->GetTokens(), IsEmpty());
+  EXPECT_THAT(token_iterator->GetTokensForTest(), IsEmpty());
 }
 
 TEST_F(PlainTokenizerTest, LastTokenAfterFullyAdvanced) {
@@ -93,7 +93,7 @@
   while (token_iterator->Advance()) {}
 
   // After advance returns false, GetTokens will stay on the last token.
-  EXPECT_THAT(token_iterator->GetTokens(),
+  EXPECT_THAT(token_iterator->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::REGULAR, "!")));
 }
 
@@ -164,6 +164,27 @@
                                EqualsToken(Token::Type::REGULAR, "World"))));
 }
 
+TEST_F(PlainTokenizerTest, ContinuousWhitespace) {
+  language_segmenter_factory::SegmenterOptions options(ULOC_US,
+                                                       jni_cache_.get());
+  ICING_ASSERT_OK_AND_ASSIGN(
+      auto language_segmenter,
+      language_segmenter_factory::Create(std::move(options)));
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<Tokenizer> plain_tokenizer,
+                             tokenizer_factory::CreateIndexingTokenizer(
+                                 StringIndexingConfig::TokenizerType::PLAIN,
+                                 language_segmenter.get()));
+
+  // Multiple continuous whitespaces will be ignored.
+  const int kNumSeparators = 256;
+  std::string text_with_spaces =
+      absl_ports::StrCat("Hello", std::string(kNumSeparators, ' '), "World");
+  EXPECT_THAT(
+      plain_tokenizer->TokenizeAll(text_with_spaces),
+      IsOkAndHolds(ElementsAre(EqualsToken(Token::Type::REGULAR, "Hello"),
+                               EqualsToken(Token::Type::REGULAR, "World"))));
+}
+
 TEST_F(PlainTokenizerTest, Punctuation) {
   language_segmenter_factory::SegmenterOptions options(ULOC_US,
                                                        jni_cache_.get());
@@ -374,7 +395,7 @@
   auto iterator = plain_tokenizer->Tokenize(kText).ValueOrDie();
 
   EXPECT_TRUE(iterator->ResetToTokenStartingAfter(0));
-  EXPECT_THAT(iterator->GetTokens(),
+  EXPECT_THAT(iterator->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::REGULAR, "b")));
 
   EXPECT_FALSE(iterator->ResetToTokenStartingAfter(2));
@@ -395,7 +416,7 @@
   auto iterator = plain_tokenizer->Tokenize(kText).ValueOrDie();
 
   EXPECT_TRUE(iterator->ResetToTokenEndingBefore(2));
-  EXPECT_THAT(iterator->GetTokens(),
+  EXPECT_THAT(iterator->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::REGULAR, "f")));
 
   EXPECT_FALSE(iterator->ResetToTokenEndingBefore(0));
@@ -441,13 +462,13 @@
 
   auto iterator = plain_tokenizer->Tokenize(kText).ValueOrDie();
   EXPECT_TRUE(iterator->Advance());
-  EXPECT_THAT(iterator->GetTokens(),
+  EXPECT_THAT(iterator->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::REGULAR, "foo")));
   for (int i = 0; i < kText.length(); ++i) {
     if (i < expected_text.size()) {
       EXPECT_TRUE(iterator->ResetToTokenStartingAfter(i));
       EXPECT_THAT(
-          iterator->GetTokens(),
+          iterator->GetTokensForTest(),
           ElementsAre(EqualsToken(Token::Type::REGULAR, expected_text[i])));
     } else {
       EXPECT_FALSE(iterator->ResetToTokenStartingAfter(i));
@@ -495,13 +516,13 @@
 
   auto iterator = plain_tokenizer->Tokenize(kText).ValueOrDie();
   EXPECT_TRUE(iterator->Advance());
-  EXPECT_THAT(iterator->GetTokens(),
+  EXPECT_THAT(iterator->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::REGULAR, "foo")));
   for (int i = kText.length() - 1; i >= 0; --i) {
     int expected_index = kText.length() - 1 - i;
     if (expected_index < expected_text.size()) {
       EXPECT_TRUE(iterator->ResetToTokenEndingBefore(i));
-      EXPECT_THAT(iterator->GetTokens(),
+      EXPECT_THAT(iterator->GetTokensForTest(),
                   ElementsAre(EqualsToken(Token::Type::REGULAR,
                                           expected_text[expected_index])));
     } else {
diff --git a/icing/tokenization/reverse_jni/reverse-jni-language-segmenter-factory.cc b/icing/tokenization/reverse_jni/reverse-jni-language-segmenter-factory.cc
index 1581e13..e6b2e70 100644
--- a/icing/tokenization/reverse_jni/reverse-jni-language-segmenter-factory.cc
+++ b/icing/tokenization/reverse_jni/reverse-jni-language-segmenter-factory.cc
@@ -28,7 +28,11 @@
 constexpr std::string_view kLocaleAmericanEnglishComputer = "en_US_POSIX";
 }  // namespace
 
-// Creates a language segmenter with the given locale.
+// Creates a language segmenter based on the provided options.
+//
+// @param options: The options for creating the language segmenter.
+// @param icu_segmenter_creation_status: Optional output parameter that will be
+//        ignored.
 //
 // Returns:
 //   A LanguageSegmenter on success
@@ -38,7 +42,7 @@
 // users. Right now illegal locale strings will be ignored by ICU. ICU
 // components will be created with its default locale.
 libtextclassifier3::StatusOr<std::unique_ptr<LanguageSegmenter>> Create(
-    SegmenterOptions options) {
+    SegmenterOptions options, StatusProto* /*icu_segmenter_creation_status*/) {
   if (options.jni_cache == nullptr) {
     return absl_ports::InvalidArgumentError(
         "Cannot create Reverse Jni Language Segmenter without a valid JniCache "
diff --git a/icing/tokenization/reverse_jni/reverse-jni-language-segmenter_test.cc b/icing/tokenization/reverse_jni/reverse-jni-language-segmenter_test.cc
index 683c5f9..7e10741 100644
--- a/icing/tokenization/reverse_jni/reverse-jni-language-segmenter_test.cc
+++ b/icing/tokenization/reverse_jni/reverse-jni-language-segmenter_test.cc
@@ -317,34 +317,6 @@
                                        "7", "8", "9")));
 }
 
-TEST_P(ReverseJniLanguageSegmenterTest, ContinuousWhitespaces) {
-  ICING_ASSERT_OK_AND_ASSIGN(
-      auto language_segmenter,
-      language_segmenter_factory::Create(
-          language_segmenter_factory::SegmenterOptions(
-              GetLocale(), jni_cache_.get())));
-  // Multiple continuous whitespaces are treated as one.
-  const int kNumSeparators = 256;
-  std::string text_with_spaces =
-      absl_ports::StrCat("Hello", std::string(kNumSeparators, ' '), "World");
-  EXPECT_THAT(language_segmenter->GetAllTerms(text_with_spaces),
-              IsOkAndHolds(ElementsAre("Hello", " ", "World")));
-
-  // Multiple continuous whitespaces are treated as one. Whitespace at the
-  // beginning of the text doesn't affect the results of GetTerm() after the
-  // iterator is done.
-  text_with_spaces = absl_ports::StrCat(std::string(kNumSeparators, ' '),
-                                        "Hello", " ", "World");
-  ICING_ASSERT_OK_AND_ASSIGN(auto itr,
-                             language_segmenter->Segment(text_with_spaces));
-  std::vector<std::string_view> terms;
-  while (itr->Advance()) {
-    terms.push_back(itr->GetTerm());
-  }
-  EXPECT_THAT(terms, ElementsAre(" ", "Hello", " ", "World"));
-  EXPECT_THAT(itr->GetTerm(), IsEmpty());
-}
-
 TEST_P(ReverseJniLanguageSegmenterTest, CJKT) {
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
@@ -775,47 +747,6 @@
   EXPECT_THAT(itr->GetTerm(), IsEmpty());
 }
 
-TEST_P(ReverseJniLanguageSegmenterTest,
-       ContinuousWhitespacesResetToTermAfterUtf32) {
-  ICING_ASSERT_OK_AND_ASSIGN(
-      auto language_segmenter,
-      language_segmenter_factory::Create(
-          language_segmenter_factory::SegmenterOptions(
-              GetLocale(), jni_cache_.get())));
-  // Multiple continuous whitespaces are treated as one.
-  constexpr std::string_view kTextWithSpace = "Hello          World";
-  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             language_segmenter->Segment(kTextWithSpace));
-
-  // String:      "Hello          World"
-  //               ^    ^         ^
-  // UTF-8 idx:    0    5         15
-  // UTF-32 idx:   0    5         15
-  EXPECT_THAT(itr->ResetToTermStartingAfterUtf32(0), IsOkAndHolds(Eq(5)));
-  EXPECT_THAT(itr->GetTerm(), Eq(" "));
-
-  EXPECT_THAT(itr->ResetToTermStartingAfterUtf32(2), IsOkAndHolds(Eq(5)));
-  EXPECT_THAT(itr->GetTerm(), Eq(" "));
-
-  EXPECT_THAT(itr->ResetToTermStartingAfterUtf32(10), IsOkAndHolds(Eq(15)));
-  EXPECT_THAT(itr->GetTerm(), Eq("World"));
-
-  EXPECT_THAT(itr->ResetToTermStartingAfterUtf32(5), IsOkAndHolds(Eq(15)));
-  EXPECT_THAT(itr->GetTerm(), Eq("World"));
-
-  EXPECT_THAT(itr->ResetToTermStartingAfterUtf32(15),
-              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
-  EXPECT_THAT(itr->GetTerm(), IsEmpty());
-
-  EXPECT_THAT(itr->ResetToTermStartingAfterUtf32(17),
-              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
-  EXPECT_THAT(itr->GetTerm(), IsEmpty());
-
-  EXPECT_THAT(itr->ResetToTermStartingAfterUtf32(19),
-              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
-  EXPECT_THAT(itr->GetTerm(), IsEmpty());
-}
-
 TEST_P(ReverseJniLanguageSegmenterTest, ChineseResetToTermAfterUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
@@ -1097,46 +1028,6 @@
   EXPECT_THAT(itr->GetTerm(), Eq("です"));
 }
 
-TEST_P(ReverseJniLanguageSegmenterTest,
-       ContinuousWhitespacesResetToTermBeforeUtf32) {
-  ICING_ASSERT_OK_AND_ASSIGN(
-      auto language_segmenter,
-      language_segmenter_factory::Create(
-          language_segmenter_factory::SegmenterOptions(
-              GetLocale(), jni_cache_.get())));
-  // Multiple continuous whitespaces are treated as one.
-  constexpr std::string_view kTextWithSpace = "Hello          World";
-  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             language_segmenter->Segment(kTextWithSpace));
-
-  // String:      "Hello          World"
-  //               ^    ^         ^
-  // UTF-8 idx:    0    5         15
-  // UTF-32 idx:   0    5         15
-  EXPECT_THAT(itr->ResetToTermEndingBeforeUtf32(0),
-              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
-  EXPECT_THAT(itr->GetTerm(), IsEmpty());
-
-  EXPECT_THAT(itr->ResetToTermEndingBeforeUtf32(2),
-              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
-  EXPECT_THAT(itr->GetTerm(), IsEmpty());
-
-  EXPECT_THAT(itr->ResetToTermEndingBeforeUtf32(10), IsOkAndHolds(Eq(0)));
-  EXPECT_THAT(itr->GetTerm(), Eq("Hello"));
-
-  EXPECT_THAT(itr->ResetToTermEndingBeforeUtf32(5), IsOkAndHolds(Eq(0)));
-  EXPECT_THAT(itr->GetTerm(), Eq("Hello"));
-
-  EXPECT_THAT(itr->ResetToTermEndingBeforeUtf32(15), IsOkAndHolds(Eq(5)));
-  EXPECT_THAT(itr->GetTerm(), Eq(" "));
-
-  EXPECT_THAT(itr->ResetToTermEndingBeforeUtf32(17), IsOkAndHolds(Eq(5)));
-  EXPECT_THAT(itr->GetTerm(), Eq(" "));
-
-  EXPECT_THAT(itr->ResetToTermEndingBeforeUtf32(19), IsOkAndHolds(Eq(5)));
-  EXPECT_THAT(itr->GetTerm(), Eq(" "));
-}
-
 TEST_P(ReverseJniLanguageSegmenterTest, ChineseResetToTermBeforeUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
diff --git a/icing/tokenization/rfc822-tokenizer.cc b/icing/tokenization/rfc822-tokenizer.cc
index 554340a..9b740c9 100644
--- a/icing/tokenization/rfc822-tokenizer.cc
+++ b/icing/tokenization/rfc822-tokenizer.cc
@@ -79,16 +79,15 @@
   //   A vector of Tokens on success
   //   An empty vector if the token list is empty
   //   An empty vector if the index is past the end of the token list
-  std::vector<Token> GetTokens() const override {
-    std::vector<Token> result;
+  void GetTokens(std::vector<Token>* out_tokens) const override {
+    out_tokens->clear();
     if (token_index_ < tokens_.size() && token_index_ >= 0) {
       int index = token_index_;
       do {
-        result.push_back(tokens_[index]);
+        out_tokens->push_back(tokens_[index]);
       } while (++index < tokens_.size() &&
                tokens_[index].type != Token::Type::RFC822_TOKEN);
     }
-    return result;
   }
 
   bool ResetToTokenStartingAfter(int32_t utf32_offset) override {
@@ -154,16 +153,16 @@
   // Returns a character iterator to the start of the token.
   libtextclassifier3::StatusOr<CharacterIterator> CalculateTokenStart()
       override {
-    CharacterIterator token_start = iterator_;
-    token_start.MoveToUtf8(GetTokens().at(0).text.begin() - text_.begin());
+    CharacterIterator token_start(text_);
+    token_start.MoveToUtf8(tokens_[token_index_].text.begin() - text_.begin());
     return token_start;
   }
 
   // Returns a character iterator to right after the end of the token.
   libtextclassifier3::StatusOr<CharacterIterator> CalculateTokenEndExclusive()
       override {
-    CharacterIterator token_end = iterator_;
-    token_end.MoveToUtf8(GetTokens().at(0).text.end() - text_.begin());
+    CharacterIterator token_end(text_);
+    token_end.MoveToUtf8(tokens_[token_index_].text.end() - text_.begin());
     return token_end;
   }
 
@@ -787,8 +786,9 @@
   ICING_ASSIGN_OR_RETURN(std::unique_ptr<Tokenizer::Iterator> iterator,
                          Tokenize(text));
   std::vector<Token> tokens;
+  std::vector<Token> batch_tokens;
   while (iterator->Advance()) {
-    std::vector<Token> batch_tokens = iterator->GetTokens();
+    iterator->GetTokens(&batch_tokens);
     tokens.insert(tokens.end(), batch_tokens.begin(), batch_tokens.end());
   }
   return tokens;
diff --git a/icing/tokenization/rfc822-tokenizer_test.cc b/icing/tokenization/rfc822-tokenizer_test.cc
index ee3a95d..d886196 100644
--- a/icing/tokenization/rfc822-tokenizer_test.cc
+++ b/icing/tokenization/rfc822-tokenizer_test.cc
@@ -33,9 +33,9 @@
   std::string text = "a@g.c";
   auto token_iterator = rfc822_tokenizer.Tokenize(text).ValueOrDie();
 
-  ASSERT_THAT(token_iterator->GetTokens(), IsEmpty());
+  ASSERT_THAT(token_iterator->GetTokensForTest(), IsEmpty());
   ASSERT_TRUE(token_iterator->Advance());
-  ASSERT_THAT(token_iterator->GetTokens(), Not(IsEmpty()));
+  ASSERT_THAT(token_iterator->GetTokensForTest(), Not(IsEmpty()));
 }
 
 TEST(Rfc822TokenizerTest, EmptyMiddleToken) {
@@ -967,10 +967,10 @@
   ASSERT_TRUE(token_iterator->Advance());
 
   ASSERT_TRUE(token_iterator->ResetToTokenStartingAfter(-1));
-  EXPECT_THAT(token_iterator->GetTokens().at(0).text, "a@g.c");
+  EXPECT_THAT(token_iterator->GetTokensForTest().at(0).text, "a@g.c");
 
   ASSERT_TRUE(token_iterator->ResetToTokenStartingAfter(5));
-  EXPECT_THAT(token_iterator->GetTokens().at(0).text, "b@g.c");
+  EXPECT_THAT(token_iterator->GetTokensForTest().at(0).text, "b@g.c");
 
   ASSERT_FALSE(token_iterator->ResetToTokenStartingAfter(6));
 }
@@ -982,7 +982,7 @@
   token_iterator->Advance();
 
   ASSERT_TRUE(token_iterator->ResetToTokenEndingBefore(5));
-  EXPECT_THAT(token_iterator->GetTokens().at(0).text, "a@g.c");
+  EXPECT_THAT(token_iterator->GetTokensForTest().at(0).text, "a@g.c");
 
   ASSERT_FALSE(token_iterator->ResetToTokenEndingBefore(4));
 }
diff --git a/icing/tokenization/tokenizer.h b/icing/tokenization/tokenizer.h
index fb7613f..778ba99 100644
--- a/icing/tokenization/tokenizer.h
+++ b/icing/tokenization/tokenizer.h
@@ -44,9 +44,10 @@
   // An iterator helping to get tokens.
   // Example usage:
   //
+  // std::vector<Token> tokens;
   // while (iterator.Advance()) {
-  //   const Token& token = iterator.GetToken();
-  //   // Do something
+  //   iterator.GetTokens(&tokens);
+  //   // Do something for each Token
   // }
   class Iterator {
    public:
@@ -55,10 +56,17 @@
     // Advances to the next token. Returns false if it has reached the end.
     virtual bool Advance() = 0;
 
-    // Returns the current token, maybe with compound tokens as well. It can be
-    // called only when Advance() returns true, otherwise an empty Token vector
-    // may be returned.
-    virtual std::vector<Token> GetTokens() const = 0;
+    // Populates `out_tokens` with the current token, maybe with compound tokens
+    // as well. It can be called only when Advance() returns true. The vector is
+    // cleared before being populated.
+    virtual void GetTokens(std::vector<Token>* out_tokens) const = 0;
+
+    // Make it faster to write/refactor tests.
+    std::vector<Token> GetTokensForTest() const {
+      std::vector<Token> result;
+      this->GetTokens(&result);
+      return result;
+    }
 
     virtual libtextclassifier3::StatusOr<CharacterIterator>
     CalculateTokenStart() {
@@ -80,7 +88,9 @@
     // iterator.ResetToTokenStartingAfter(4);
     // // The first full token starting after position 4 (the 'b' in "bar") is
     // // "baz".
-    // PrintToken(iterator.GetToken());  // prints "baz"
+    // std::vector<Token> tokens;
+    // iterator.GetTokens(&tokens);
+    // PrintToken(tokens[0]);  // prints "baz"
     virtual bool ResetToTokenStartingAfter(int32_t utf32_offset) {
       return false;
     }
@@ -93,7 +103,9 @@
     // iterator.ResetToTokenEndingBefore(4);
     // // The first full token ending before position 4 (the 'b' in "bar") is
     // // "foo".
-    // PrintToken(iterator.GetToken());  // prints "foo"
+    // std::vector<Token> tokens;
+    // iterator.GetTokens(&tokens);
+    // PrintToken(tokens[0]);  // prints "baz"
     virtual bool ResetToTokenEndingBefore(int32_t utf32_offset) {
       return false;
     }
diff --git a/icing/tokenization/trigram-tokenizer.cc b/icing/tokenization/trigram-tokenizer.cc
index 870a75d..745b773 100644
--- a/icing/tokenization/trigram-tokenizer.cc
+++ b/icing/tokenization/trigram-tokenizer.cc
@@ -60,15 +60,16 @@
     return true;
   }
 
-  std::vector<Token> GetTokens() const override {
+  void GetTokens(std::vector<Token>* out_tokens) const override {
     int start_byte_idx = utf8_char_itrs_.front().utf8_index();
     int end_byte_idx =
         utf8_char_itrs_.back().utf8_index() +
         i18n_utils::GetUtf8Length(utf8_char_itrs_.back().GetCurrentChar());
 
-    return {Token(/*type_in=*/Token::Type::TRIGRAM,
-                  /*text_in=*/text_.substr(start_byte_idx,
-                                           end_byte_idx - start_byte_idx))};
+    out_tokens->assign(
+        {Token(/*type_in=*/Token::Type::TRIGRAM,
+               /*text_in=*/text_.substr(start_byte_idx,
+                                        end_byte_idx - start_byte_idx))});
   };
 
   // Returns a character iterator to the start of the current trigram token.
@@ -279,8 +280,9 @@
   ICING_ASSIGN_OR_RETURN(std::unique_ptr<Tokenizer::Iterator> iterator,
                          Tokenize(text));
   std::vector<Token> tokens;
+  std::vector<Token> batch_tokens;
   while (iterator->Advance()) {
-    std::vector<Token> batch_tokens = iterator->GetTokens();
+    iterator->GetTokens(&batch_tokens);
     tokens.insert(tokens.end(), batch_tokens.begin(), batch_tokens.end());
   }
   return tokens;
diff --git a/icing/tokenization/trigram-tokenizer_test.cc b/icing/tokenization/trigram-tokenizer_test.cc
index a1466c5..fbb8831 100644
--- a/icing/tokenization/trigram-tokenizer_test.cc
+++ b/icing/tokenization/trigram-tokenizer_test.cc
@@ -189,7 +189,7 @@
 
   // Advance to "abc".
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "abc")));
   ICING_ASSERT_OK_AND_ASSIGN(CharacterIterator start_itr0,
                              itr->CalculateTokenStart());
@@ -200,7 +200,7 @@
 
   // Advance to "bcd".
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "bcd")));
   ICING_ASSERT_OK_AND_ASSIGN(CharacterIterator start_itr1,
                              itr->CalculateTokenStart());
@@ -221,7 +221,7 @@
 
   // Advance to "我每天".
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "我每天")));
   ICING_ASSERT_OK_AND_ASSIGN(CharacterIterator start_itr0,
                              itr->CalculateTokenStart());
@@ -232,7 +232,7 @@
 
   // Advance to "每天走".
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "每天走")));
   ICING_ASSERT_OK_AND_ASSIGN(CharacterIterator start_itr1,
                              itr->CalculateTokenStart());
@@ -257,7 +257,7 @@
   ASSERT_THAT(itr->Advance(), IsTrue());
   ASSERT_THAT(itr->Advance(), IsTrue());
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "efg")));
 
   // Reset backward to after utf32_offset 1.
@@ -265,22 +265,22 @@
 
   // Verify the iterator is pointing to "cde" since it is the leftmost trigram
   // token after utf32_offset 1.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "cde")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "def".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "def")));
   // Advance to "efg".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "efg")));
   // Advance to "fgh".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "fgh")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -291,26 +291,26 @@
 
   // Verify the iterator is pointing to "bcd" since it is the leftmost trigram
   // token after utf32_offset 0.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "bcd")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "cde".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "cde")));
   // Advance to "def".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "def")));
   // Advance to "efg".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "efg")));
   // Advance to "fgh".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "fgh")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -329,7 +329,7 @@
   ASSERT_THAT(itr->Advance(), IsTrue());
   ASSERT_THAT(itr->Advance(), IsTrue());
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "路去上")));
 
   // Reset backward to after "每". Note that the utf32_offset of "每" is 1.
@@ -337,22 +337,22 @@
 
   // Verify the iterator is pointing to "天走路" since it is the leftmost
   // trigram token after "每".
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "天走路")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "走路去".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "走路去")));
   // Advance to "路去上".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "路去上")));
   // Advance to "去上班".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "去上班")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -363,26 +363,26 @@
 
   // Verify the iterator is pointing to "每天走" since it is the leftmost
   // trigram token after utf32_offset 0.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "每天走")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "天走路".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "天走路")));
   // Advance to "走路去".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "走路去")));
   // Advance to "路去上".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "路去上")));
   // Advance to "去上班".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "去上班")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -397,7 +397,7 @@
 
   // Advance 1 time to "abc".
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "abc")));
 
   // Reset forward to after utf32_offset 2.
@@ -405,18 +405,18 @@
 
   // Verify the iterator is pointing to "def" since it is the leftmost trigram
   // token after utf32_offset 2.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "def")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "efg".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "efg")));
   // Advance to "fgh".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "fgh")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -431,7 +431,7 @@
 
   // Advance 1 time to "我每天".
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "我每天")));
 
   // Reset forward to after "天". Note that the utf32_offset of "天" is 2.
@@ -439,18 +439,18 @@
 
   // Verify the iterator is pointing to "走路去" since it is the leftmost
   // trigram token after "天".
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "走路去")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "路去上".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "路去上")));
   // Advance to "去上班".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "去上班")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -467,7 +467,7 @@
   // Advance twice to "bcd".
   ASSERT_THAT(itr->Advance(), IsTrue());
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "bcd")));
 
   // Reset to after utf32_offset -1.
@@ -475,18 +475,18 @@
 
   // Verify the iterator is pointing to "abc" since it is the leftmost trigram
   // token after utf32_offset -1.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "abc")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "bcd".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "bcd")));
   // Advance to "cde".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "cde")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -496,18 +496,18 @@
 
   // Verify the iterator is pointing to "abc" since it is the leftmost trigram
   // token after utf32_offset -2.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "abc")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "bcd".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "bcd")));
   // Advance to "cde".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "cde")));
 }
 
@@ -522,7 +522,7 @@
   // Advance twice to "bcd".
   ASSERT_THAT(itr->Advance(), IsTrue());
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "bcd")));
 
   // Reset to after utf32_offset 2. Since the last token is "cde" and the
@@ -556,7 +556,7 @@
 
   // Advance once to "我每天".
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "我每天")));
 
   // Reset to after "天" (with utf32_offset 2). Since the last token is
@@ -595,14 +595,14 @@
 
   // Verify the iterator is pointing to "cde" since it is the leftmost trigram
   // token after utf32_offset 1.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "cde")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "def".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "def")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -622,14 +622,14 @@
 
   // Verify the iterator is pointing to "天走路" since it is the leftmost
   // trigram token after utf32_offset 1.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "天走路")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "走路去".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "走路去")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -705,14 +705,14 @@
 
   // Verify the iterator is pointing to "cde" since it is the leftmost trigram
   // token after utf32_offset 1.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "cde")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "def".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "def")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -739,14 +739,14 @@
 
   // Verify the iterator is pointing to "天走路" since it is the leftmost
   // trigram token after utf32_offset 1.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "天走路")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "走路去".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "走路去")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -822,7 +822,7 @@
   ASSERT_THAT(itr->Advance(), IsTrue());
   ASSERT_THAT(itr->Advance(), IsTrue());
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "efg")));
 
   // Reset backward to before utf32_offset 5.
@@ -830,22 +830,22 @@
 
   // Verify the iterator is pointing to "cde" since it is the rightmost trigram
   // token before utf32_offset 5.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "cde")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "def".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "def")));
   // Advance to "efg".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "efg")));
   // Advance to "fgh".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "fgh")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -864,7 +864,7 @@
   ASSERT_THAT(itr->Advance(), IsTrue());
   ASSERT_THAT(itr->Advance(), IsTrue());
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "路去上")));
 
   // Reset backward to before "去". Note that the utf32_offset of "去" is 5.
@@ -872,22 +872,22 @@
 
   // Verify the iterator is pointing to "天走路" since it is the rightmost
   // trigram token before "每".
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "天走路")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "走路去".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "走路去")));
   // Advance to "路去上".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "路去上")));
   // Advance to "去上班".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "去上班")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -902,7 +902,7 @@
 
   // Advance 1 time to "abc".
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "abc")));
 
   // Reset forward to before utf32_offset 6.
@@ -910,18 +910,18 @@
 
   // Verify the iterator is pointing to "def" since it is the rightmost trigram
   // token before utf32_offset 6.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "def")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "efg".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "efg")));
   // Advance to "fgh".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "fgh")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -936,7 +936,7 @@
 
   // Advance 1 time to "我每天".
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "我每天")));
 
   // Reset forward to before "上". Note that the utf32_offset of "上" is 6.
@@ -944,18 +944,18 @@
 
   // Verify the iterator is pointing to "走路去" since it is the rightmost
   // trigram token before "上".
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "走路去")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "路去上".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "路去上")));
   // Advance to "去上班".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "去上班")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -972,7 +972,7 @@
 
   // Advance once to "abc".
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "abc")));
 
   // Reset to before utf32_offset 5. Since the last token "cde" ends at
@@ -982,7 +982,7 @@
 
   // Verify the iterator is pointing to "cde" since it is the rightmost trigram
   // token before utf32_offset 5.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "cde")));
 
   // Since it is the last token, the next Advance() should fail.
@@ -998,7 +998,7 @@
 
   // Verify the iterator is pointing to "cde" since it is the rightmost trigram
   // token before utf32_offset 6.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "cde")));
 
   // Since it is the last token, the next Advance() should fail.
@@ -1016,7 +1016,7 @@
 
   // Advance once to "我每天".
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "我每天")));
 
   // Reset to before utf32_offset 5. Since the last token "天走路" ends at
@@ -1026,7 +1026,7 @@
 
   // Verify the iterator is pointing to "天走路" since it is the rightmost
   // trigram token before utf32_offset 5.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "天走路")));
 
   // Since it is the last token, the next Advance() should fail.
@@ -1042,7 +1042,7 @@
 
   // Verify the iterator is pointing to "天走路" since it is the rightmost
   // trigram token before utf32_offset 6.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "天走路")));
 
   // Since it is the last token, the next Advance() should fail.
@@ -1060,7 +1060,7 @@
   // Advance twice to "bcd".
   ASSERT_THAT(itr->Advance(), IsTrue());
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "bcd")));
 
   // Reset to before utf32_offset 2. Since the first token is "abc" and the
@@ -1091,7 +1091,7 @@
 
   // Advance once to "我每天".
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "我每天")));
 
   // Reset to before "天" (with utf32_offset 2). Since the first token is
@@ -1126,14 +1126,14 @@
 
   // Verify the iterator is pointing to "cde" since it is the rightmost trigram
   // token before utf32_offset 5.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "cde")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "def".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "def")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -1153,14 +1153,14 @@
 
   // Verify the iterator is pointing to "天走路" since it is the rightmost
   // trigram token before utf32_offset 5.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "天走路")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "走路去".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "走路去")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -1240,14 +1240,14 @@
 
   // Verify the iterator is pointing to "cde" since it is the rightmost trigram
   // token before utf32_offset 5.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "cde")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "def".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "def")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -1274,14 +1274,14 @@
 
   // Verify the iterator is pointing to "天走路" since it is the rightmost
   // trigram token before utf32_offset 5.
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "天走路")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "走路去".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "走路去")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -1360,29 +1360,29 @@
   ASSERT_THAT(itr->Advance(), IsTrue());
   ASSERT_THAT(itr->Advance(), IsTrue());
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "cde")));
 
   // Call ResetToStart().
   EXPECT_THAT(itr->ResetToStart(), IsTrue());
 
   // Verify the iterator is pointing to the start trigram "abc".
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "abc")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "bcd".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "bcd")));
   // Advance to "cde".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "cde")));
   // Advance to "def".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "def")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -1399,29 +1399,29 @@
   ASSERT_THAT(itr->Advance(), IsTrue());
   ASSERT_THAT(itr->Advance(), IsTrue());
   ASSERT_THAT(itr->Advance(), IsTrue());
-  ASSERT_THAT(itr->GetTokens(),
+  ASSERT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "天走路")));
 
   // Call ResetToStart().
   EXPECT_THAT(itr->ResetToStart(), IsTrue());
 
   // Verify the iterator is pointing to the start trigram "我每天".
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "我每天")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "每天走".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "每天走")));
   // Advance to "天走路".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "天走路")));
   // Advance to "走路去".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "走路去")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -1439,18 +1439,18 @@
   EXPECT_THAT(itr->ResetToStart(), IsTrue());
 
   // Verify the iterator is pointing to "abc".
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "abc")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "bcd".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "bcd")));
   // Advance to "cde".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "cde")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -1468,18 +1468,18 @@
   EXPECT_THAT(itr->ResetToStart(), IsTrue());
 
   // Verify the iterator is pointing to "我每天".
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "我每天")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "每天走".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "每天走")));
   // Advance to "天走路".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "天走路")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -1543,22 +1543,22 @@
   EXPECT_THAT(itr->ResetToStart(), IsTrue());
 
   // Verify the iterator is pointing to the start trigram "abc".
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "abc")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "bcd".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "bcd")));
   // Advance to "cde".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "cde")));
   // Advance to "def".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "def")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
@@ -1583,22 +1583,22 @@
   EXPECT_THAT(itr->ResetToStart(), IsTrue());
 
   // Verify the iterator is pointing to the start trigram "我每天".
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "我每天")));
 
   // Verify the iterator is still valid and able to advance to the rest of the
   // tokens.
   // Advance to "每天走".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "每天走")));
   // Advance to "天走路".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "天走路")));
   // Advance to "走路去".
   EXPECT_THAT(itr->Advance(), IsTrue());
-  EXPECT_THAT(itr->GetTokens(),
+  EXPECT_THAT(itr->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::TRIGRAM, "走路去")));
 
   EXPECT_THAT(itr->Advance(), IsFalse());
diff --git a/icing/tokenization/verbatim-tokenizer.cc b/icing/tokenization/verbatim-tokenizer.cc
index b7745f8..1b6bd31 100644
--- a/icing/tokenization/verbatim-tokenizer.cc
+++ b/icing/tokenization/verbatim-tokenizer.cc
@@ -44,14 +44,11 @@
     return true;
   }
 
-  std::vector<Token> GetTokens() const override {
-    std::vector<Token> result;
-
+  void GetTokens(std::vector<Token>* out_tokens) const override {
+    out_tokens->clear();
     if (!term_.empty() && has_advanced_to_end_) {
-      result.push_back(Token(Token::Type::VERBATIM, term_));
+      out_tokens->emplace_back(Token::Type::VERBATIM, term_);
     }
-
-    return result;
   }
 
   libtextclassifier3::StatusOr<CharacterIterator> CalculateTokenStart()
@@ -148,9 +145,10 @@
   ICING_ASSIGN_OR_RETURN(std::unique_ptr<Tokenizer::Iterator> iterator,
                          Tokenize(text));
   std::vector<Token> tokens;
+  std::vector<Token> batch_tokens;
   while (iterator->Advance()) {
-    std::vector<Token> batch = iterator->GetTokens();
-    tokens.insert(tokens.end(), batch.begin(), batch.end());
+    iterator->GetTokens(&batch_tokens);
+    tokens.insert(tokens.end(), batch_tokens.begin(), batch_tokens.end());
   }
   return tokens;
 }
diff --git a/icing/tokenization/verbatim-tokenizer_test.cc b/icing/tokenization/verbatim-tokenizer_test.cc
index 8e4418b..3fff6f2 100644
--- a/icing/tokenization/verbatim-tokenizer_test.cc
+++ b/icing/tokenization/verbatim-tokenizer_test.cc
@@ -97,7 +97,7 @@
   auto token_iterator = verbatim_tokenizer->Tokenize(kText).ValueOrDie();
 
   // We should get no tokens if we get the token before advancing.
-  EXPECT_THAT(token_iterator->GetTokens(), IsEmpty());
+  EXPECT_THAT(token_iterator->GetTokensForTest(), IsEmpty());
 }
 
 TEST_F(VerbatimTokenizerTest, ResetToTokenEndingBefore) {
@@ -112,13 +112,13 @@
   // Reset to beginning of verbatim of token. We provide an offset of 13 as it
   // is larger than the final index (12) of the verbatim token.
   EXPECT_TRUE(token_iterator->ResetToTokenEndingBefore(13));
-  EXPECT_THAT(token_iterator->GetTokens(),
+  EXPECT_THAT(token_iterator->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::VERBATIM, "Hello, world!")));
 
   // Ensure our cached character iterator propertly maintains the end of the
   // verbatim token.
   EXPECT_TRUE(token_iterator->ResetToTokenEndingBefore(13));
-  EXPECT_THAT(token_iterator->GetTokens(),
+  EXPECT_THAT(token_iterator->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::VERBATIM, "Hello, world!")));
 
   // We should not be able to reset with an offset before or within
@@ -138,7 +138,7 @@
 
   // Get token without resetting
   EXPECT_TRUE(token_iterator->Advance());
-  EXPECT_THAT(token_iterator->GetTokens(),
+  EXPECT_THAT(token_iterator->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::VERBATIM, "Hello, world!")));
 
   // We expect a sole verbatim token, so it's not possible to reset after the
@@ -148,7 +148,7 @@
   // We expect to be reset to the sole verbatim token when the offset is
   // negative.
   EXPECT_TRUE(token_iterator->ResetToTokenStartingAfter(-1));
-  EXPECT_THAT(token_iterator->GetTokens(),
+  EXPECT_THAT(token_iterator->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::VERBATIM, "Hello, world!")));
 }
 
@@ -163,12 +163,12 @@
 
   // Get token without resetting
   EXPECT_TRUE(token_iterator->Advance());
-  EXPECT_THAT(token_iterator->GetTokens(),
+  EXPECT_THAT(token_iterator->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::VERBATIM, "Hello, world!")));
 
   // Retrieve token again after resetting to start
   EXPECT_TRUE(token_iterator->ResetToStart());
-  EXPECT_THAT(token_iterator->GetTokens(),
+  EXPECT_THAT(token_iterator->GetTokensForTest(),
               ElementsAre(EqualsToken(Token::Type::VERBATIM, "Hello, world!")));
 }
 
diff --git a/icing/transform/icu/icu-normalizer-factory.cc b/icing/transform/icu/icu-normalizer-factory.cc
index 290fbcc..ebc6e97 100644
--- a/icing/transform/icu/icu-normalizer-factory.cc
+++ b/icing/transform/icu/icu-normalizer-factory.cc
@@ -23,25 +23,37 @@
 #include "icing/transform/icu/icu-normalizer.h"
 #include "icing/transform/normalizer-options.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/status-util.h"
 
 namespace icing {
 namespace lib {
 
 namespace normalizer_factory {
 
-// Creates an ICU-based normalizer.
+using ::icing::lib::status_util::TransformStatus;
+
+// Creates an ICU-based normalizer based on the provided options.
+//
+// @param options: The options for creating the normalizer.
+// @param icu_normalizer_creation_status: Optional output parameter that will be
+//        populated with the status of IcuNormalizer.
 //
 // Returns:
 //   A normalizer on success
 //   INVALID_ARGUMENT_ERROR if options.max_term_byte_size <= 0
 //   INTERNAL_ERROR on errors
 libtextclassifier3::StatusOr<std::unique_ptr<Normalizer>> Create(
-    const NormalizerOptions& options) {
+    const NormalizerOptions& options,
+    StatusProto* icu_normalizer_creation_status) {
   if (options.max_term_byte_size <= 0) {
     return absl_ports::InvalidArgumentError(
         "max_term_byte_size must be greater than zero.");
   }
-  return IcuNormalizer::Create(options.max_term_byte_size);
+  auto icu_normalizer_or = IcuNormalizer::Create(options.max_term_byte_size);
+  if (icu_normalizer_creation_status != nullptr) {
+    TransformStatus(icu_normalizer_or.status(), icu_normalizer_creation_status);
+  }
+  return icu_normalizer_or;
 }
 
 }  // namespace normalizer_factory
diff --git a/icing/transform/icu/icu-normalizer_test.cc b/icing/transform/icu/icu-normalizer_test.cc
index 499c7c1..1d1e63d 100644
--- a/icing/transform/icu/icu-normalizer_test.cc
+++ b/icing/transform/icu/icu-normalizer_test.cc
@@ -46,6 +46,15 @@
   std::unique_ptr<Normalizer> normalizer_;
 };
 
+TEST_F(IcuNormalizerTest, SuccessfulIcuNormalizerSetsOkStatus) {
+  StatusProto icu_normalizer_creation_status;
+  NormalizerOptions options(/*max_term_byte_size=*/1024);
+  ICING_ASSERT_OK_AND_ASSIGN(
+      auto normalizer,
+      normalizer_factory::Create(options, &icu_normalizer_creation_status));
+  EXPECT_THAT(icu_normalizer_creation_status.code(), Eq(StatusProto::OK));
+}
+
 TEST_F(IcuNormalizerTest, Creation) {
   NormalizerOptions options1(/*max_term_byte_size=*/5);
   EXPECT_THAT(normalizer_factory::Create(options1), IsOk());
diff --git a/icing/transform/map/map-normalizer-factory.cc b/icing/transform/map/map-normalizer-factory.cc
index 2c0dc8d..e012303 100644
--- a/icing/transform/map/map-normalizer-factory.cc
+++ b/icing/transform/map/map-normalizer-factory.cc
@@ -16,6 +16,7 @@
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/canonical_errors.h"
+#include "icing/proto/status.pb.h"
 #include "icing/transform/map/map-normalizer.h"
 #include "icing/transform/normalizer-options.h"
 #include "icing/transform/normalizer.h"
@@ -25,14 +26,19 @@
 
 namespace normalizer_factory {
 
-// Creates a map-based normalizer.
+// Creates a map-based normalizer based on the provided options.
+//
+// @param options: The options for creating the normalizer.
+// @param icu_normalizer_creation_status: Optional output parameter that will be
+//        ignored.
 //
 // Returns:
 //   A normalizer on success
 //   INVALID_ARGUMENT_ERROR if options.max_term_byte_size <= 0
 //   INTERNAL_ERROR on errors
 libtextclassifier3::StatusOr<std::unique_ptr<Normalizer>> Create(
-    const NormalizerOptions& options) {
+    const NormalizerOptions& options,
+    StatusProto* /*icu_normalizer_creation_status*/) {
   if (options.max_term_byte_size <= 0) {
     return absl_ports::InvalidArgumentError(
         "normalizer_max_term_byte_size must be greater than zero.");
diff --git a/icing/transform/normalizer-factory.h b/icing/transform/normalizer-factory.h
index 6a9709c..0935c8d 100644
--- a/icing/transform/normalizer-factory.h
+++ b/icing/transform/normalizer-factory.h
@@ -18,6 +18,7 @@
 #include <memory>
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/proto/status.pb.h"
 #include "icing/transform/normalizer-options.h"
 #include "icing/transform/normalizer.h"
 
@@ -26,14 +27,20 @@
 
 namespace normalizer_factory {
 
-// Creates a normalizer.
+// Creates a normalizer based on the provided options.
+//
+// @param options: The options for creating the normalizer.
+// @param icu_normalizer_creation_status: Optional output parameter that will be
+//        populated with the status of IcuNormalizer if it's creation is
+//        attempted.
 //
 // Returns:
 //   A normalizer on success
 //   INVALID_ARGUMENT if options.max_term_byte_size <= 0
 //   INTERNAL_ERROR on errors
 libtextclassifier3::StatusOr<std::unique_ptr<Normalizer>> Create(
-    const NormalizerOptions& options);
+    const NormalizerOptions& options,
+    StatusProto* icu_normalizer_creation_status = nullptr);
 
 }  // namespace normalizer_factory
 
diff --git a/icing/util/crc32.cc b/icing/util/crc32.cc
index c182f31..a8a6553 100644
--- a/icing/util/crc32.cc
+++ b/icing/util/crc32.cc
@@ -46,6 +46,11 @@
   return crc_;
 }
 
+uint32_t Crc32::Combine(const Crc32& other_crc, int other_data_size) {
+  crc_ = crc32_combine(crc_, other_crc.Get(), other_data_size);
+  return crc_;
+}
+
 libtextclassifier3::StatusOr<uint32_t> Crc32::UpdateWithXor(
     const std::string_view xored_str, int full_data_size, int position) {
   // For appending, use Append().
diff --git a/icing/util/crc32.h b/icing/util/crc32.h
index dfa7756..ab65eb1 100644
--- a/icing/util/crc32.h
+++ b/icing/util/crc32.h
@@ -62,6 +62,16 @@
   // Crc32(base_crc).Append(str) is not the same as zlib::crc32(base_crc, str);
   uint32_t Append(std::string_view str);
 
+  // Combines other_crc to the current crc for bytes concatenation. The caller
+  // must provide the raw data (byte) size of other_crc.
+  //
+  // NOTE: all the following are equivalent:
+  // 1) crc32.Append(str_a + str_b);
+  // 2) crc32.Append(str_a); crc32.Append(str_b);
+  // 3) crc32.Append(str_a); crc32_b.Append(str_b);
+  //    crc32.Combine(crc32_b, str_b.size());
+  uint32_t Combine(const Crc32& other_crc, int other_data_size);
+
   // Update a string's rolling crc when some content is modified in the middle
   // at an offset. We need the xored_str, which is the new value xored with the
   // original value.
diff --git a/icing/util/crc32_test.cc b/icing/util/crc32_test.cc
index d6f4c46..5351b7a 100644
--- a/icing/util/crc32_test.cc
+++ b/icing/util/crc32_test.cc
@@ -30,7 +30,9 @@
 namespace lib {
 
 namespace {
+
 using ::testing::Eq;
+using ::testing::Ne;
 
 void UpdateAtRandomOffset(std::string* buf, uint32_t* update_xor, int* offset) {
   // The max value of rand() is 2^31 - 1 (2147483647) but the max value of
@@ -79,6 +81,60 @@
   EXPECT_THAT(crc32_foo_and_bar.Get(), Eq(crc32_foobar.Get()));
 }
 
+TEST(Crc32Test, Combine) {
+  Crc32 crc32_foo;
+  crc32_foo.Append("foo");
+
+  Crc32 crc32_barbaz;
+  crc32_barbaz.Append("barbaz");
+
+  Crc32 crc32_foobarbaz;
+  crc32_foobarbaz.Append("foobarbaz");
+
+  EXPECT_THAT(crc32_foo.Combine(crc32_barbaz, 6), Eq(crc32_foobarbaz.Get()));
+  EXPECT_THAT(crc32_foo.Get(), Eq(crc32_foobarbaz.Get()));
+}
+
+TEST(Crc32Test, Combine_incorrectResultWithWrongSize) {
+  Crc32 crc32_foo;
+  crc32_foo.Append("foo");
+
+  Crc32 crc32_barbaz;
+  crc32_barbaz.Append("barbaz");
+
+  Crc32 crc32_foobarbaz;
+  crc32_foobarbaz.Append("foobarbaz");
+
+  int wrong_size = 5;
+  EXPECT_THAT(crc32_foo.Combine(crc32_barbaz, wrong_size),
+              Ne(crc32_foobarbaz.Get()));
+  EXPECT_THAT(crc32_foo.Get(), Ne(crc32_foobarbaz.Get()));
+}
+
+TEST(Crc32Test, Combine_withEmptyString) {
+  Crc32 crc32_foo;
+  crc32_foo.Append("foo");
+  uint32_t crc_foo_val = crc32_foo.Get();
+
+  Crc32 crc32_empty;
+  crc32_empty.Append("");
+
+  EXPECT_THAT(crc32_foo.Combine(crc32_empty, 0), Eq(crc_foo_val));
+  EXPECT_THAT(crc32_foo.Get(), Eq(crc_foo_val));
+}
+
+TEST(Crc32Test, Combine_toEmptyString) {
+  Crc32 crc32_empty;
+  crc32_empty.Append("");
+
+  Crc32 crc32_foo;
+  crc32_foo.Append("foo");
+  uint32_t crc_foo_val = crc32_foo.Get();
+
+  EXPECT_THAT(crc32_empty.Combine(crc32_foo, 3), Eq(crc_foo_val));
+  EXPECT_THAT(crc32_empty.Get(), Eq(crc_foo_val));
+}
+
 TEST(Crc32Test, UpdateAtPosition) {
   std::string buf;
   buf.resize(1000);
diff --git a/icing/util/document-util.cc b/icing/util/document-util.cc
new file mode 100644
index 0000000..d3834e6
--- /dev/null
+++ b/icing/util/document-util.cc
@@ -0,0 +1,36 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/util/document-util.h"
+
+#include <utility>
+
+#include "icing/proto/document.pb.h"
+#include "icing/proto/document_wrapper.pb.h"
+
+namespace icing {
+namespace lib {
+
+namespace document_util {
+
+DocumentWrapper CreateDocumentWrapper(DocumentProto document) {
+  DocumentWrapper document_wrapper;
+  *document_wrapper.mutable_document() = std::move(document);
+  return document_wrapper;
+}
+
+}  // namespace document_util
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/util/document-util.h b/icing/util/document-util.h
new file mode 100644
index 0000000..c9e117a
--- /dev/null
+++ b/icing/util/document-util.h
@@ -0,0 +1,34 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef ICING_UTIL_DOCUMENT_UTIL_H_
+#define ICING_UTIL_DOCUMENT_UTIL_H_
+
+#include "icing/proto/document.pb.h"
+#include "icing/proto/document_wrapper.pb.h"
+
+namespace icing {
+namespace lib {
+
+namespace document_util {
+
+// Creates a DocumentWrapper from a DocumentProto.
+DocumentWrapper CreateDocumentWrapper(DocumentProto document);
+
+}  // namespace document_util
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_UTIL_DOCUMENT_UTIL_H_
diff --git a/icing/util/document-util_test.cc b/icing/util/document-util_test.cc
new file mode 100644
index 0000000..c93ad6d
--- /dev/null
+++ b/icing/util/document-util_test.cc
@@ -0,0 +1,47 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/util/document-util.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "icing/document-builder.h"
+#include "icing/portable/equals-proto.h"
+#include "icing/proto/document_wrapper.pb.h"
+
+namespace icing {
+namespace lib {
+namespace document_util {
+
+namespace {
+
+using ::icing::lib::portable_equals_proto::EqualsProto;
+
+TEST(DocumentUtilTest, CreateDocumentWrapper) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("icing", "fake_type/1")
+                               .SetSchema("FakeType")
+                               .AddStringProperty("prop1", "foo", "bar", "baz")
+                               .Build();
+
+  DocumentWrapper document_wrapper = CreateDocumentWrapper(document);
+  EXPECT_THAT(document_wrapper.document(), EqualsProto(document));
+}
+
+}  // namespace
+
+}  // namespace document_util
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/util/document-validator.cc b/icing/util/document-validator.cc
index 916b34e..8c68efa 100644
--- a/icing/util/document-validator.cc
+++ b/icing/util/document-validator.cc
@@ -80,7 +80,7 @@
   auto type_config_or = schema_store_->GetSchemaTypeConfig(document.schema());
   if (!type_config_or.ok()) {
     ICING_LOG(ERROR) << type_config_or.status().error_message()
-                     << "Error while validating document ("
+                     << " Error while validating document ("
                      << document.namespace_() << ", " << document.uri() << ")";
     return type_config_or.status();
   }
@@ -114,7 +114,8 @@
           document.namespace_(), ", ", document.uri(),
           ") of type: ", document.schema(), "."));
     }
-    const PropertyConfigProto& property_config = *property_iter->second;
+    const PropertyConfigProto& property_config =
+        *property_iter->second.property_config;
 
     // Get the property value size according to data type.
     int value_size = 0;
diff --git a/icing/util/embedding-util.h b/icing/util/embedding-util.h
index 5026051..1390d4e 100644
--- a/icing/util/embedding-util.h
+++ b/icing/util/embedding-util.h
@@ -15,6 +15,7 @@
 #ifndef ICING_UTIL_EMBEDDING_UTIL_H_
 #define ICING_UTIL_EMBEDDING_UTIL_H_
 
+#include <array>
 #include <string_view>
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
@@ -27,6 +28,15 @@
 
 namespace embedding_util {
 
+// The list of embedding query metric types, with the order matching the
+// enum value.
+static const std::array<SearchSpecProto::EmbeddingQueryMetricType::Code, 3>
+    kEmbeddingQueryMetricTypes = {
+        SearchSpecProto::EmbeddingQueryMetricType::COSINE,       // value = 1
+        SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT,  // value = 2
+        SearchSpecProto::EmbeddingQueryMetricType::EUCLIDEAN     // value = 3
+};
+
 inline libtextclassifier3::StatusOr<
     SearchSpecProto::EmbeddingQueryMetricType::Code>
 GetEmbeddingQueryMetricTypeFromName(std::string_view metric_name) {
diff --git a/icing/util/logging.cc b/icing/util/logging.cc
index f60526b..fe14be0 100644
--- a/icing/util/logging.cc
+++ b/icing/util/logging.cc
@@ -64,6 +64,11 @@
 // The last 16 bits represent the current verbosity.
 std::atomic<uint32_t> global_logging_level = DEFAULT_LOGGING_LEVEL;
 
+// TODO(b/401363381): Remove this once we have a better way to log to
+// /dev/hvc2 in isolated storage.
+// Indicate whether we should force logging to /dev/hvc2 for ICING_LOG.
+std::atomic<bool> global_force_debug_logs = false;
+
 }  // namespace
 
 // Whether we should log according to the current logging level.
@@ -102,11 +107,30 @@
   return true;
 }
 
+// TODO(b/401363381): Remove this once we have a better way to log to
+// /dev/hvc2 in isolated storage.
+void SetForceDebugLogging(bool force) {
+  // Using the relaxed order for better performance because we only need to
+  // guarantee the atomicity for this specific statement, without the need to
+  // worry about reordering.
+  global_force_debug_logs.store(force, std::memory_order_relaxed);
+}
+
+// TODO(b/401363381): Remove this once we have a better way to log to
+// /dev/hvc2 in isolated storage.
+bool GetForceDebugLogging() {
+  // Using the relaxed order for better performance because we only need to
+  // guarantee the atomicity for this specific statement, without the need to
+  // worry about reordering.
+  return global_force_debug_logs.load(std::memory_order_relaxed);
+}
+
 LogMessage::LogMessage(LogSeverity::Code severity, uint16_t verbosity,
                        const char *file_name, int line_number)
     : severity_(severity),
       verbosity_(verbosity),
       should_log_(ShouldLog(severity_, verbosity_)),
+      force_debug_logs_(GetForceDebugLogging()),
       stream_(should_log_) {
   if (should_log_) {
     stream_ << JumpToBasename(file_name) << ":" << line_number << ": ";
@@ -115,7 +139,8 @@
 
 LogMessage::~LogMessage() {
   if (should_log_) {
-    LowLevelLogging(severity_, kIcingLoggingTag, stream_.message);
+    LowLevelLogging(severity_, kIcingLoggingTag, stream_.message,
+                    force_debug_logs_);
   }
   if (severity_ == LogSeverity::FATAL) {
     std::terminate();  // Will print a stacktrace (stdout or logcat).
diff --git a/icing/util/logging.h b/icing/util/logging.h
index 23280dc..7eaf776 100644
--- a/icing/util/logging.h
+++ b/icing/util/logging.h
@@ -39,6 +39,16 @@
 // The function will always return false when verbosity is negative.
 bool SetLoggingLevel(LogSeverity::Code severity, int16_t verbosity = 0);
 
+// TODO(b/401363381): Remove this once we have a better way to log to
+// /dev/hvc2 in isolated storage.
+// Indicate whether we should force logging to /dev/hvc2 for ICING_LOG.
+void SetForceDebugLogging(bool force);
+
+// TODO(b/401363381): Remove this once we have a better way to log to
+// /dev/hvc2 in isolated storage.
+// Get whether or not we should force logging to /dev/hvc2 for ICING_LOG.
+bool GetForceDebugLogging();
+
 // A tiny code footprint string stream for assembling log messages.
 struct LoggingStringStream {
   explicit LoggingStringStream(bool should_log) : should_log_(should_log) {}
@@ -68,6 +78,14 @@
 }
 
 inline LoggingStringStream& operator<<(LoggingStringStream& stream,
+                                       char* message) {
+  if (stream.should_log_) {
+    stream.message.append(message);
+  }
+  return stream;
+}
+
+inline LoggingStringStream& operator<<(LoggingStringStream& stream,
                                        const char* message) {
   if (stream.should_log_) {
     stream.message.append(message);
@@ -84,6 +102,14 @@
 }
 
 inline LoggingStringStream& operator<<(LoggingStringStream& stream,
+                                       std::string& message) {
+  if (stream.should_log_) {
+    stream.message.append(message);
+  }
+  return stream;
+}
+
+inline LoggingStringStream& operator<<(LoggingStringStream& stream,
                                        std::string_view message) {
   if (stream.should_log_) {
     stream.message.append(message);
@@ -123,6 +149,7 @@
   const LogSeverity::Code severity_;
   const uint16_t verbosity_;
   const bool should_log_;
+  const bool force_debug_logs_;
 
   // Stream that "prints" all info into a string (not to a file).  We construct
   // here the entire logging message and next print it in one operation.
diff --git a/icing/util/logging_raw.cc b/icing/util/logging_raw.cc
index 44dd000..09d0308 100644
--- a/icing/util/logging_raw.cc
+++ b/icing/util/logging_raw.cc
@@ -50,7 +50,7 @@
 }  // namespace
 
 void LowLevelLogging(LogSeverity::Code severity, const std::string& tag,
-                     const std::string& message) {
+                     const std::string& message, const bool force_debug_logs) {
   const int android_log_level = GetAndroidLogLevel(severity);
 #if __ANDROID_API__ >= 30
   if (!__android_log_is_loggable(android_log_level, tag.c_str(),
@@ -58,7 +58,28 @@
     return;
   }
 #endif  // __ANDROID_API__ >= 30
-  __android_log_write(android_log_level, tag.c_str(), message.c_str());
+  // TODO(b/401363381): Remove this once we have a better way to log to
+  // /dev/hvc2 in isolated storage.
+  if (force_debug_logs) {
+    // When isolated icing storage is enabled, the VM debug level determines
+    // whether icing debug logs are delivered. We want the icing debug logs
+    // to always be present. Thus force logging to /dev/hvc2.
+    const char* file_logger_path = "/dev/hvc2";
+    // "e" opens the file with the O_CLOEXEC flag. Icing should not be starting
+    // any other processes, but it is added as a precaution.
+    static FILE* stream = [&file_logger_path]() {
+      FILE* f = fopen(file_logger_path, "ae");
+      if (f != nullptr) {
+        return f;
+      }
+      fprintf(stderr, "Failed to open /dev/hvc2 for logging. "
+                      "Falling back to stderr.\n");
+      return stderr;
+    }();
+    fprintf(stream, "%s: %s\n", tag.c_str(), message.c_str());
+  } else {
+    __android_log_write(android_log_level, tag.c_str(), message.c_str());
+  }
 }
 
 }  // namespace lib
@@ -91,7 +112,7 @@
 }  // namespace
 
 void LowLevelLogging(LogSeverity::Code severity, const std::string &tag,
-                     const std::string &message) {
+                     const std::string &message, const bool force_debug_logs) {
   // TODO(b/146903474) Do not log to stderr for logs other than FATAL and ERROR.
   fprintf(stderr, "[%s] %s : %s\n", LogSeverityToString(severity), tag.c_str(),
           message.c_str());
diff --git a/icing/util/logging_raw.h b/icing/util/logging_raw.h
index 99dddb6..438e1a8 100644
--- a/icing/util/logging_raw.h
+++ b/icing/util/logging_raw.h
@@ -26,7 +26,7 @@
 // severity.  From android/log.h: "the tag normally corresponds to the component
 // that emits the log message, and should be reasonably small".
 void LowLevelLogging(LogSeverity::Code severity, const std::string &tag,
-                     const std::string &message);
+                     const std::string &message, const bool force_debug_logs);
 
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/util/logging_test.cc b/icing/util/logging_test.cc
index eac018e..29e39b5 100644
--- a/icing/util/logging_test.cc
+++ b/icing/util/logging_test.cc
@@ -13,6 +13,8 @@
 // limitations under the License.
 
 #include "icing/util/logging.h"
+#include <cerrno>
+#include <cstring>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
@@ -153,6 +155,18 @@
   EXPECT_THAT(stream2.message, IsEmpty());
 }
 
+TEST(LoggingTest, LoggingStreamStrErrorTest) {
+  ASSERT_TRUE(SetLoggingLevel(LogSeverity::INFO));
+  char* error_message = strerror(ENOENT);
+  // This one should be logged.
+  LoggingStringStream stream1 = (ICING_LOG(INFO) << error_message);
+  EXPECT_THAT(stream1.message, EndsWith("No such file or directory"));
+
+  // This one should not be logged, thus empty.
+  LoggingStringStream stream2 = (ICING_LOG(DBG) << error_message);
+  EXPECT_THAT(stream2.message, IsEmpty());
+}
+
 }  // namespace
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/util/math-util.h b/icing/util/math-util.h
index 22f429f..8aab34e 100644
--- a/icing/util/math-util.h
+++ b/icing/util/math-util.h
@@ -17,6 +17,8 @@
 
 #include <cstdint>
 #include <limits>
+#include <utility>
+#include <vector>
 
 namespace icing {
 namespace lib {
@@ -91,6 +93,38 @@
   return n;
 }
 
+// Applies a permutation to multiple containers in-place, which permutes
+// elements within the given containers according to the provided permutation
+// vector. permutation[i] specifies the new index for the element originally at
+// index i.
+template <typename... ContainerTypes>
+static void ApplyPermutation(const std::vector<int> &permutation,
+                             ContainerTypes &...values) {
+  std::vector<bool> done(permutation.size());
+  // Apply permutation
+  for (int i = 0; i < permutation.size(); ++i) {
+    if (done[i]) {
+      continue;
+    }
+    done[i] = true;
+    int curr = i;
+    int next = permutation[i];
+    // Since every finite permutation is formed by disjoint cycles, we can
+    // start with the current element, at index i, and swap the element at
+    // this position with whatever element that *should* be here. Then,
+    // continue to swap the original element, at its updated positions, with
+    // the element that should be occupying that position until the original
+    // element has reached *its* correct position. This completes applying the
+    // single cycle in the permutation.
+    while (next != i) {
+      (std::swap(values[curr], values[next]), ...);
+      done[next] = true;
+      curr = next;
+      next = permutation[next];
+    }
+  }
+}
+
 }  // namespace math_util
 
 }  // namespace lib
diff --git a/icing/util/simple-task-scheduler.cc b/icing/util/simple-task-scheduler.cc
new file mode 100644
index 0000000..cc3fae5
--- /dev/null
+++ b/icing/util/simple-task-scheduler.cc
@@ -0,0 +1,230 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/util/simple-task-scheduler.h"
+
+#include <chrono>              // NOLINT
+#include <condition_variable>  // NOLINT
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <mutex>   // NOLINT
+#include <thread>  // NOLINT
+#include <utility>
+
+#include "icing/util/clock.h"
+
+namespace icing {
+namespace lib {
+
+std::unique_ptr<SimpleTaskScheduler> SimpleTaskScheduler::Create(
+    const Clock& clock) {
+  auto scheduler =
+      std::unique_ptr<SimpleTaskScheduler>(new SimpleTaskScheduler(clock));
+  scheduler->Initialize();
+  return scheduler;
+}
+
+SimpleTaskScheduler::~SimpleTaskScheduler() {
+  // Notify the thread to terminate.
+  {
+    std::unique_lock<std::mutex> lk(mutex_);  // NOLINT
+
+    is_terminated_ = true;
+  }
+  cv_.notify_all();
+
+  if (thread_ != nullptr) {
+    thread_->join();
+  }
+}
+
+void SimpleTaskScheduler::ScheduleAt(TaskId task_id, std::function<void()> task,
+                                     int64_t new_execution_timestamp_ms) {
+  if (new_execution_timestamp_ms < 0) {
+    new_execution_timestamp_ms = kInvalidTimestampMs;
+  }
+
+  // - Check if the task id exists.
+  //   - If yes, then overwrite the TaskInfo for the task id.
+  //   - If no, then create a new task info and add it into the map.
+  // - Notify the executor thread blocking at wait() or wait_for() to wake up
+  //   and wait for the new execution time.
+  {
+    std::unique_lock<std::mutex> lk(mutex_);  // NOLINT
+
+    auto itr = tasks_.find(task_id);
+    if (itr != tasks_.end()) {
+      itr->second->task =
+          std::make_shared<std::function<void()>>(std::move(task));
+      itr->second->execution_timestamp_ms = new_execution_timestamp_ms;
+    } else {
+      tasks_.insert(
+          {task_id, std::make_unique<TaskInfo>(std::move(task),
+                                               new_execution_timestamp_ms)});
+    }
+  }
+  cv_.notify_all();
+}
+
+void SimpleTaskScheduler::Cancel(TaskId task_id) {
+  {
+    std::unique_lock<std::mutex> lk(mutex_);  // NOLINT
+
+    auto itr = tasks_.find(task_id);
+    if (itr == tasks_.end()) {
+      return;
+    }
+
+    // Set the timestamp to kInvalidTimestampMs to cancel the task.
+    //
+    // Note: we CANNOT remove the entry from the map here. Suppose the executor
+    //   thread is currently waiting for the execution of THIS task (i.e.
+    //   blocking at wait_for()):
+    // - We need kInvalidTimestampMs as a mark to tell the executor thread not
+    //   to execute the task after waking up.
+    // - Also in this case, the executor thread currently holds a TaskInfo
+    //   pointer to THIS task in a local variable. Removing the task entry from
+    //   the map here will cause pointer instability and crash after waking up.
+    //
+    // Therefore, we keep the entry in the map, and delegate the "cleanup" work
+    // to the executor thread.
+    itr->second->execution_timestamp_ms = kInvalidTimestampMs;
+  }
+  cv_.notify_all();
+}
+
+int64_t SimpleTaskScheduler::GetScheduledTimeMs(TaskId task_id) const {
+  std::unique_lock<std::mutex> lk(mutex_);  // NOLINT
+
+  auto itr = tasks_.find(task_id);
+  if (itr != tasks_.end()) {
+    return itr->second->execution_timestamp_ms;
+  }
+  return kInvalidTimestampMs;
+}
+
+void SimpleTaskScheduler::Initialize() {
+  std::unique_lock<std::mutex> lk(mutex_);  // NOLINT
+
+  thread_ = std::make_unique<std::thread>([this]() {
+    while (true) {
+      std::shared_ptr<std::function<void()>> next_task = nullptr;
+
+      {
+        std::unique_lock<std::mutex> lk(mutex_);  // NOLINT
+
+        if (is_terminated_) {
+          break;
+        }
+
+        CleanUpCompletedTasksLocked();
+
+        int64_t current_time_ms = clock_.GetSystemTimeMilliseconds();
+        TaskInfo* next_task_info = GetNextTaskInfoLocked();
+        if (next_task_info == nullptr) {
+          // No scheduled task. Wait for the next task being scheduled. This
+          // will avoid busy-waiting.
+          cv_.wait(lk);
+        } else if (next_task_info->execution_timestamp_ms > current_time_ms) {
+          // Wait for the execution time. It is possible that wait_for() is
+          // interrupted by ScheduleAt(), Cancel(), or spurious wakeup.
+          //
+          // Note: we use wait_for() instead of wait_until() for the convenience
+          //   of mocking the clock in the unit test.
+          std::cv_status wait_status = cv_.wait_for(
+              lk,
+              std::chrono::milliseconds(next_task_info->execution_timestamp_ms -
+                                        current_time_ms));
+          // If the thread is woken up by timeout, execute the task.
+          // Otherwise, the thread is woken up by ScheduleAt(), Cancel() or
+          // spurious wakeup.
+          if (wait_status != std::cv_status::timeout) {
+            // Set the pointer to nullptr indicating that we need another round
+            // of wait() before executing it.
+            next_task_info = nullptr;
+          }
+        }
+
+        // It is possible that is_terminated_ is modified during wait() or
+        // wait_for(). Check it again here.
+        if (is_terminated_) {
+          break;
+        }
+
+        // It is possible that the task is canceled or rescheduled during
+        // wait_for().
+        // - In most cases, wait_status above will NOT be
+        //   std::cv_status::timeout and we should've already set next_task_info
+        //   to nullptr.
+        // - However, it is possible that the cancel/reschedule operation took
+        //   place right at the wait_for() timeout. Therefore, let's check
+        //   execution_timestamp_ms again here.
+        if (next_task_info != nullptr &&
+            next_task_info->execution_timestamp_ms != kInvalidTimestampMs) {
+          // Reset the timestamp before executing the task.
+          next_task_info->execution_timestamp_ms = kInvalidTimestampMs;
+
+          // Get a shared pointer to the task.
+          //
+          // Note: since we execute the task outside of the critical section,
+          //   it is possible that the task is overwritten between the gap of
+          //   the critical section and the actual execution below (i.e. the
+          //   caller calls ScheduleAt() with the same task id + a different
+          //   task implementation and overwrites the task object). Here, we
+          //   have to execute the original task, so we need to use a shared
+          //   pointer for it.
+          next_task = next_task_info->task;
+        }
+      }
+
+      if (next_task != nullptr) {
+        // It is possible that the task is expensive or attempts to acquire
+        // the mutex again and schedule another task, so execute the current one
+        // outside of the critical section to avoid blocking or deadlock.
+        (*next_task)();
+      }
+    }
+  });
+}
+
+SimpleTaskScheduler::TaskInfo* SimpleTaskScheduler::GetNextTaskInfoLocked()
+    const {
+  TaskInfo* next_task_info = nullptr;
+  for (const auto& [_, task_info] : tasks_) {
+    if (task_info->execution_timestamp_ms == kInvalidTimestampMs) {
+      continue;
+    }
+
+    if (next_task_info == nullptr ||
+        task_info->execution_timestamp_ms <
+            next_task_info->execution_timestamp_ms) {
+      next_task_info = task_info.get();
+    }
+  }
+  return next_task_info;
+}
+
+void SimpleTaskScheduler::CleanUpCompletedTasksLocked() {
+  for (auto itr = tasks_.begin(); itr != tasks_.end();) {
+    if (itr->second->execution_timestamp_ms == kInvalidTimestampMs) {
+      itr = tasks_.erase(itr);
+    } else {
+      ++itr;
+    }
+  }
+}
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/util/simple-task-scheduler.h b/icing/util/simple-task-scheduler.h
new file mode 100644
index 0000000..6bbd30e
--- /dev/null
+++ b/icing/util/simple-task-scheduler.h
@@ -0,0 +1,170 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef ICING_UTIL_SIMPLE_TASK_SCHEDULER_H_
+#define ICING_UTIL_SIMPLE_TASK_SCHEDULER_H_
+
+#include <condition_variable>  // NOLINT
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <mutex>   // NOLINT
+#include <thread>  // NOLINT
+#include <unordered_map>
+#include <utility>
+
+#include "icing/util/clock.h"
+
+namespace icing {
+namespace lib {
+
+// A utility class to schedule custom tasks to fire at a specific timestamp.
+//
+// Example:
+//   Clock clock;
+//   std::unique_ptr<SimpleTaskScheduler> scheduler =
+//       SimpleTaskScheduler::Create(clock);
+//
+//   // Schedule task_id1 to fire in 5 seconds.
+//   SimpleTaskScheduler::TaskId task_id1 = 1;
+//   scheduler->ScheduleAt(
+//       task_id1,
+//       [&]() { FooApi(); },
+//       clock.GetSystemTimeMilliseconds() + 5000);
+//
+//   // Schedule task_id2 to fire in 1 second.
+//   SimpleTaskScheduler::TaskId task_id2 = 2;
+//   scheduler->ScheduleAt(
+//       task_id2,
+//       [&]() { BarApi(); },
+//       clock.GetSystemTimeMilliseconds() + 1000);
+//
+// Then, task_id2 will fire first (running BarApi()), and then task_id1 will
+// fire afterwards (running FooApi()).
+//
+// Note: tasks are executed sequentially and there is only one execution at a
+// time.
+// - Tasks may be blocked by other requests to Icing.
+// - A task scheduled for time T will execute at the greater of T or the
+//   earliest time at which no previously scheduled tasks or incoming api calls
+//   are running.
+class SimpleTaskScheduler {
+ public:
+  using TaskId = int32_t;
+
+  // Creates a SimpleTaskScheduler.
+  //
+  // Note: after creation, no task is scheduled yet. It is only active after
+  //   calling ScheduleAt().
+  static std::unique_ptr<SimpleTaskScheduler> Create(const Clock& clock);
+
+  ~SimpleTaskScheduler();
+
+  // Disables copy, move, and assignment.
+  SimpleTaskScheduler(const SimpleTaskScheduler&) = delete;
+  SimpleTaskScheduler& operator=(const SimpleTaskScheduler&) = delete;
+  SimpleTaskScheduler(SimpleTaskScheduler&&) = delete;
+  SimpleTaskScheduler& operator=(SimpleTaskScheduler&&) = delete;
+
+  // (Re)Schedules the task (given the task id) with the given new timestamp in
+  // milliseconds.
+  // - If there is a pending task of the same id, then its execution time will
+  //   be overwritten by new_execution_timestamp_ms and the pending task is
+  //   rescheduled. The task function is also overwritten.
+  // - If new_execution_timestamp_ms is not greater than the current time
+  //   (clock_.GetSystemTimeMilliseconds()), an execution will fire without
+  //   waiting (as long as there is no ongoing execution).
+  // - If the timestamp is invalid (smaller than 0), the previous scheduled task
+  //   of this id will be canceled.
+  // - If it is time to run another new scheduled task but there is still an
+  //   ongoing execution, then the new scheduled task will wait until the
+  //   ongoing execution finishes and fire after that.
+  void ScheduleAt(TaskId task_id, std::function<void()> task,
+                  int64_t new_execution_timestamp_ms);
+
+  // Cancels the task by the task id.
+  // - It only cancels the pending task and does not interrupt an ongoing
+  //   (already started) execution.
+  // - If no task was scheduled or the task id is invalid, then this is a no-op.
+  void Cancel(TaskId task_id);
+
+  // Returns the scheduled time (next execution time) of the task in
+  // milliseconds. If the task id is unregistered or there is no scheduled
+  // execution, then returns -1.
+  int64_t GetScheduledTimeMs(TaskId task_id) const;
+
+ private:
+  static constexpr int64_t kInvalidTimestampMs = -1;
+
+  struct TaskInfo {
+    std::shared_ptr<std::function<void()>> task;
+    int64_t execution_timestamp_ms;
+
+    explicit TaskInfo(std::function<void()> task_in,
+                      int64_t execution_timestamp_ms_in)
+        : task(std::make_shared<std::function<void()>>(std::move(task_in))),
+          execution_timestamp_ms(execution_timestamp_ms_in) {}
+  };
+
+  explicit SimpleTaskScheduler(const Clock& clock)
+      : clock_(clock), is_terminated_(false) {};
+
+  // Helper function to initialize the scheduler.
+  void Initialize();
+
+  // Gets the next task info to execute.
+  //
+  // Returns:
+  //   - nullptr if there is no scheduled task.
+  //   - Otherwise, a valid TaskInfo pointer for the next scheduled task. i.e.
+  //     the task info with the smallest valid execution timestamp of all
+  //     scheduled tasks.
+  SimpleTaskScheduler::TaskInfo* GetNextTaskInfoLocked() const;
+
+  // Helper function to clean up completed or canceled tasks (i.e. tasks with
+  // invalid execution timestamp).
+  //
+  // Note: this function MUST be called only by the executor thread.
+  void CleanUpCompletedTasksLocked();
+
+  // The executor thread for waiting for the execution time and executing the
+  // task.
+  //
+  // Use unique_ptr to delay initialization the thread.
+  std::unique_ptr<std::thread> thread_;
+
+  // Mutex and condition variable for the thread to wait and be notified.
+  mutable std::mutex mutex_;  // NOLINT
+  std::condition_variable cv_;
+
+  // The clock used to get the current time.
+  const Clock& clock_;  // Does not own.
+
+  // Shared memory between the main thread and the executor thread.
+  // - The main thread will use public methods to update the values and notify
+  //   the executor thread via cv_.
+  // - The executor thread will use the values to determine the next action
+  //   after being notified.
+
+  // Mapping scheduled task id to info.
+  std::unordered_map<TaskId, std::unique_ptr<TaskInfo>> tasks_;
+
+  // Whether the scheduler is about to be destroyed or not.
+  bool is_terminated_;
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_UTIL_SIMPLE_TASK_SCHEDULER_H_
diff --git a/icing/util/simple-task-scheduler_test.cc b/icing/util/simple-task-scheduler_test.cc
new file mode 100644
index 0000000..197bfab
--- /dev/null
+++ b/icing/util/simple-task-scheduler_test.cc
@@ -0,0 +1,504 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/util/simple-task-scheduler.h"
+
+#include <atomic>
+#include <chrono>  // NOLINT
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <mutex>
+#include <thread>  // NOLINT
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "icing/testing/fake-clock.h"
+#include "icing/util/clock.h"
+
+namespace icing {
+namespace lib {
+
+namespace {
+
+using ::testing::Eq;
+
+constexpr SimpleTaskScheduler::TaskId kTaskId = 0x12345678;
+
+// The epsilon for sleep() in milliseconds. There may be a small delay between
+// the scheduled task firing time and std::this_thread::sleep_for in the unit
+// test, so we need to sleep a little longer to make sure the task completes.
+constexpr int64_t kSleepMsEps = 100;
+
+// Simulate a class holds and uses the scheduler object to fire its own API
+// call.
+class MainClass {
+ public:
+  explicit MainClass(const Clock& clock) : clock_(clock) {}
+
+  void Initialize() {
+    std::unique_lock<std::mutex> lk(mutex_);  // NOLINT
+
+    scheduler_ = SimpleTaskScheduler::Create(clock_);
+    scheduler_->ScheduleAt(kTaskId, CreateTask(),
+                           clock_.GetSystemTimeMilliseconds() + 1000);
+  }
+
+  void FooApi() {
+    std::unique_lock<std::mutex> lk(mutex_);  // NOLINT
+
+    ++counter_;
+    if (scheduler_ != nullptr) {
+      // Re-schedule a new task to fire in 1 second.
+      scheduler_->ScheduleAt(kTaskId, CreateTask(),
+                             clock_.GetSystemTimeMilliseconds() + 1000);
+    }
+  }
+
+  int GetCounter() {
+    std::unique_lock<std::mutex> lk(mutex_);  // NOLINT
+
+    return counter_;
+  }
+
+ private:
+  std::function<void()> CreateTask() {
+    return [this]() { FooApi(); };
+  }
+
+  std::mutex mutex_;  // NOLINT
+
+  const Clock& clock_;  // Does not own.
+  std::unique_ptr<SimpleTaskScheduler> scheduler_;
+  std::atomic<int> counter_ = 0;
+};
+
+TEST(SimpleTaskSchedulerTest, SimpleScheduleAndExecute) {
+  Clock clock;
+  std::atomic<int> counter = 0;
+
+  std::unique_ptr<SimpleTaskScheduler> scheduler =
+      SimpleTaskScheduler::Create(clock);
+
+  // Schedule the task to fire in 1 second.
+  scheduler->ScheduleAt(
+      kTaskId, /*task=*/[&counter]() { ++counter; },
+      clock.GetSystemTimeMilliseconds() + 1000);
+
+  // After 1 second, the task should fire and increment the counter.
+  std::this_thread::sleep_for(std::chrono::milliseconds(1000 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(1));
+}
+
+TEST(SimpleTaskSchedulerTest, ScheduleAt_rescheduleTask) {
+  Clock clock;
+  std::atomic<int> counter = 0;
+  const auto task = [&counter]() { ++counter; };
+
+  std::unique_ptr<SimpleTaskScheduler> scheduler =
+      SimpleTaskScheduler::Create(clock);
+
+  // Schedule a task to fire in 3 seconds.
+  scheduler->ScheduleAt(kTaskId, task,
+                        clock.GetSystemTimeMilliseconds() + 3000);
+
+  // Sleep for 100 ms and reschedule the task (with the same id) to fire in 1
+  // second.
+  std::this_thread::sleep_for(std::chrono::milliseconds(100));
+  scheduler->ScheduleAt(kTaskId, task,
+                        clock.GetSystemTimeMilliseconds() + 1000);
+
+  // - After 1 second, the task should fire and increment the counter.
+  // - Although ScheduleAt() interrupts wait_for(), the task should not be
+  //   executed, and the counter should be only incremented once.
+  std::this_thread::sleep_for(std::chrono::milliseconds(1000 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(1));
+
+  // Sleep for another 3 seconds. The counter should remain 1. This verifies
+  // the original scheduled task (3 seconds) is canceled.
+  std::this_thread::sleep_for(std::chrono::milliseconds(3000 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(1));
+}
+
+TEST(SimpleTaskSchedulerTest, ScheduleAt_overwriteTask) {
+  Clock clock;
+  std::atomic<int> counter = 0;
+  const auto task1 = [&counter]() { ++counter; };
+  const auto task2 = [&counter]() { counter += 10; };
+
+  std::unique_ptr<SimpleTaskScheduler> scheduler =
+      SimpleTaskScheduler::Create(clock);
+
+  // Schedule a task to fire in 3 seconds.
+  scheduler->ScheduleAt(kTaskId, task1,
+                        clock.GetSystemTimeMilliseconds() + 3000);
+
+  // Sleep for 100 ms and reschedule the task to fire in 1 second, with a
+  // different task implementation.
+  std::this_thread::sleep_for(std::chrono::milliseconds(100));
+  scheduler->ScheduleAt(kTaskId, task2,
+                        clock.GetSystemTimeMilliseconds() + 1000);
+
+  // - After 1 second, the task should fire.
+  // - It should execute the 2nd task, so the counter is incremented by 10.
+  std::this_thread::sleep_for(std::chrono::milliseconds(1000 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(10));
+
+  // Sleep for another 3 seconds. The counter should remain 10. This verifies
+  // the original scheduled task (3 seconds) is not executed.
+  std::this_thread::sleep_for(std::chrono::milliseconds(3000 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(10));
+}
+
+TEST(SimpleTaskSchedulerTest, ScheduleAt_negativeTimestampShouldCancel) {
+  Clock clock;
+  std::atomic<int> counter = 0;
+  const auto task = [&counter]() { ++counter; };
+
+  std::unique_ptr<SimpleTaskScheduler> scheduler =
+      SimpleTaskScheduler::Create(clock);
+
+  // Schedule a task to fire in 1 second.
+  scheduler->ScheduleAt(kTaskId, task,
+                        clock.GetSystemTimeMilliseconds() + 1000);
+
+  // Schedule again with a negative timestamp to cancel the previous task. The
+  // scheduled time should be updated to -1, which means the task is canceled.
+  scheduler->ScheduleAt(kTaskId, task, -100);
+  EXPECT_THAT(scheduler->GetScheduledTimeMs(kTaskId), Eq(-1));
+
+  // The task should NOT fire.
+  std::this_thread::sleep_for(std::chrono::milliseconds(1000 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(0));
+}
+
+TEST(SimpleTaskSchedulerTest,
+     ScheduleAt_timestampEarlierThanNowShouldFireImmediately) {
+  Clock clock;
+  std::atomic<int> counter = 0;
+  const auto task = [&counter]() { ++counter; };
+
+  std::unique_ptr<SimpleTaskScheduler> scheduler =
+      SimpleTaskScheduler::Create(clock);
+
+  // Schedule a task with a timestamp earlier than now.
+  scheduler->ScheduleAt(kTaskId, task,
+                        clock.GetSystemTimeMilliseconds() - 1000);
+
+  // The task should fire immediately and increment the counter.
+  std::this_thread::sleep_for(std::chrono::milliseconds(kSleepMsEps));
+  EXPECT_THAT(counter, Eq(1));
+}
+
+TEST(SimpleTaskSchedulerTest, Cancel) {
+  Clock clock;
+  std::atomic<int> counter = 0;
+  const auto task = [&counter]() { ++counter; };
+
+  std::unique_ptr<SimpleTaskScheduler> scheduler =
+      SimpleTaskScheduler::Create(clock);
+
+  // Schedule a task to fire in 1 second.
+  scheduler->ScheduleAt(kTaskId, task,
+                        clock.GetSystemTimeMilliseconds() + 1000);
+
+  // Cancel the task after 100 ms.
+  std::this_thread::sleep_for(std::chrono::milliseconds(100));
+  scheduler->Cancel(kTaskId);
+
+  // - After 1 second, the task should not fire and the counter should remain 0.
+  // - Although Cancel() interrupts wait_for(), the task should not be executed,
+  //   and the counter should not be incremented.
+  std::this_thread::sleep_for(std::chrono::milliseconds(1000 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(0));
+}
+
+TEST(SimpleTaskSchedulerTest, GetScheduledTimeMs) {
+  FakeClock fake_clock;
+  fake_clock.SetSystemTimeMilliseconds(0);
+  const auto task = []() {
+    // no-op
+  };
+
+  std::unique_ptr<SimpleTaskScheduler> scheduler =
+      SimpleTaskScheduler::Create(fake_clock);
+
+  // Before scheduling, the scheduled time should be -1 (invalid timestamp).
+  EXPECT_THAT(scheduler->GetScheduledTimeMs(kTaskId), Eq(-1));
+
+  scheduler->ScheduleAt(kTaskId, task, 100000);
+  EXPECT_THAT(scheduler->GetScheduledTimeMs(kTaskId), Eq(100000));
+
+  scheduler->ScheduleAt(kTaskId, task, 200000);
+  EXPECT_THAT(scheduler->GetScheduledTimeMs(kTaskId), Eq(200000));
+
+  scheduler->ScheduleAt(kTaskId, task, 300000);
+  EXPECT_THAT(scheduler->GetScheduledTimeMs(kTaskId), Eq(300000));
+
+  scheduler->Cancel(kTaskId);
+  EXPECT_THAT(scheduler->GetScheduledTimeMs(kTaskId), Eq(-1));
+
+  scheduler->ScheduleAt(kTaskId, task, 400000);
+  EXPECT_THAT(scheduler->GetScheduledTimeMs(kTaskId), Eq(400000));
+
+  scheduler->ScheduleAt(kTaskId, task, -1000);
+  EXPECT_THAT(scheduler->GetScheduledTimeMs(kTaskId), Eq(-1));
+
+  // Unregistered task id should return -1.
+  EXPECT_THAT(scheduler->GetScheduledTimeMs(kTaskId + 1), Eq(-1));
+}
+
+TEST(SimpleTaskSchedulerTest, Destructor) {
+  Clock clock;
+  std::atomic<int> counter = 0;
+  const auto task = [&counter]() { ++counter; };
+
+  std::unique_ptr<SimpleTaskScheduler> scheduler =
+      SimpleTaskScheduler::Create(clock);
+
+  // Schedule a task to fire in 1 second.
+  scheduler->ScheduleAt(kTaskId, task,
+                        clock.GetSystemTimeMilliseconds() + 1000);
+
+  // Destruct the scheduler after 100 ms.
+  std::this_thread::sleep_for(std::chrono::milliseconds(100));
+  scheduler.reset();
+
+  // After 1 second, the task should not fire and the counter should remain 0.
+  std::this_thread::sleep_for(std::chrono::milliseconds(1000 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(0));
+}
+
+TEST(SimpleTaskSchedulerTest, Destructor_shouldCancelAndJoinThread) {
+  Clock clock;
+  std::atomic<int> counter = 0;
+  const auto task = [&counter]() { ++counter; };
+
+  std::unique_ptr<SimpleTaskScheduler> scheduler =
+      SimpleTaskScheduler::Create(clock);
+
+  // Schedule a task to fire in 1 second.
+  scheduler->ScheduleAt(kTaskId, task,
+                        clock.GetSystemTimeMilliseconds() + 1000);
+
+  // Destruct the scheduler object immediately. The background thread should be
+  // joined successfully in the destructor, without any crash or deadlock.
+  scheduler.reset();
+
+  EXPECT_THAT(counter, Eq(0));
+}
+
+TEST(SimpleTaskSchedulerTest, Destructor_noScheduledTask) {
+  Clock clock;
+  std::atomic<int> counter = 0;
+
+  std::unique_ptr<SimpleTaskScheduler> scheduler =
+      SimpleTaskScheduler::Create(clock);
+
+  // Destruct the scheduler object immediately without scheduling any task.
+  // There should be no crash or deadlock.
+  scheduler.reset();
+
+  EXPECT_THAT(counter, Eq(0));
+}
+
+TEST(SimpleTaskSchedulerTest, Destructor_noDeadlock) {
+  Clock clock;
+  std::atomic<int> counter = 0;
+  const auto task = [&counter]() {
+    ++counter;
+    std::this_thread::sleep_for(std::chrono::milliseconds(3000));
+  };
+
+  // Create a scheduler and schedule an expensive task that sleeps for 3
+  // seconds.
+  //
+  // This allows the test to trigger a different execution order (race
+  // condition) and verify the correctness of the conditional variable
+  // notification in the destructor before joining the thread, and
+  // is_terminated_ flag check.
+  std::unique_ptr<SimpleTaskScheduler> scheduler =
+      SimpleTaskScheduler::Create(clock);
+
+  // Schedule a task to fire in 1 second.
+  scheduler->ScheduleAt(kTaskId, task,
+                        clock.GetSystemTimeMilliseconds() + 1000);
+
+  // Sleep for 1 second.
+  // - At this moment, the executor thread is still running the expensive task.
+  // - Destruct the scheduler object at this moment.
+  //   - The destructor will set is_terminated_ to true and notify the executor
+  //     thread to terminate, but the executor thread is still running the
+  //     expensive task, so the notification is no-op.
+  //   - The destructor will join the executor thread.
+  //   - After the executor thread finishes the expensive task, it should not
+  //     call wait() or wait_for() again. Instead, it should terminate and join
+  //     the destructor.
+  std::this_thread::sleep_for(std::chrono::milliseconds(1000 + kSleepMsEps));
+  scheduler.reset();
+
+  EXPECT_THAT(counter, Eq(1));
+}
+
+TEST(SimpleTaskSchedulerTest, MultipleScheduledTasks) {
+  Clock clock;
+  std::atomic<int> counter = 0;
+
+  std::unique_ptr<SimpleTaskScheduler> scheduler =
+      SimpleTaskScheduler::Create(clock);
+
+  SimpleTaskScheduler::TaskId task_id1 = 1;
+  SimpleTaskScheduler::TaskId task_id2 = 2;
+  // Schedule task_id1 to fire in 2 seconds.
+  int64_t current_time_ms = clock.GetSystemTimeMilliseconds();
+  scheduler->ScheduleAt(
+      task_id1, [&counter]() { ++counter; }, current_time_ms + 2000);
+  // Schedule task_id2 to fire in 1 second.
+  scheduler->ScheduleAt(
+      task_id2, [&counter]() { counter += 10; }, current_time_ms + 1000);
+
+  // After 1 second, task_id2 should fire and increment the counter by 10.
+  std::this_thread::sleep_for(std::chrono::milliseconds(1000 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(10));
+
+  // After another 1 second, task_id1 should fire and increment the counter
+  // by 1.
+  std::this_thread::sleep_for(std::chrono::milliseconds(1000 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(11));
+}
+
+TEST(SimpleTaskSchedulerTest, ExpensiveTask_nextExecutionShouldWaitAndExecute) {
+  Clock clock;
+  std::atomic<int> counter = 0;
+  const auto task = [&counter]() {
+    ++counter;
+    std::this_thread::sleep_for(std::chrono::milliseconds(2000));
+  };
+
+  // Create a scheduler and schedule an expensive task that sleeps for 2
+  // seconds.
+  //
+  // This allows the test to trigger a different execution order (race
+  // condition) and verify the correctness of the conditional variable
+  // notification in the destructor before joining the thread, and
+  // is_terminated_ flag check.
+  std::unique_ptr<SimpleTaskScheduler> scheduler =
+      SimpleTaskScheduler::Create(clock);
+
+  // Schedule a task to fire immediately. The counter is incremented, but the
+  // 1st execution is still running.
+  scheduler->ScheduleAt(kTaskId, task, clock.GetSystemTimeMilliseconds());
+  std::this_thread::sleep_for(std::chrono::milliseconds(kSleepMsEps));
+  EXPECT_THAT(counter, Eq(1));
+
+  // Schedule another task to fire in 10 ms for the 2nd execution.
+  scheduler->ScheduleAt(kTaskId, task, clock.GetSystemTimeMilliseconds() + 10);
+  // Sleep for 10 ms. The 1st execution is still running, so the 2nd execution
+  // should wait and run after the 1st execution finishes.
+  std::this_thread::sleep_for(std::chrono::milliseconds(10 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(1));
+
+  // Finally, the 1st execution finishes and the 2nd execution fires.
+  std::this_thread::sleep_for(std::chrono::milliseconds(2000 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(2));
+}
+
+TEST(SimpleTaskSchedulerTest,
+     ExpensiveTask_multipleSchedulesDuringOngoingExecutionFiresOnceAfterwards) {
+  Clock clock;
+  std::atomic<int> counter = 0;
+  const auto task = [&counter]() {
+    ++counter;
+    std::this_thread::sleep_for(std::chrono::milliseconds(2000));
+  };
+
+  // Create a scheduler and schedule an expensive task that sleeps for 2
+  // seconds.
+  //
+  // This allows the test to trigger a different execution order (race
+  // condition) and verify the correctness of the conditional variable
+  // notification in the destructor before joining the thread, and
+  // is_terminated_ flag check.
+  std::unique_ptr<SimpleTaskScheduler> scheduler =
+      SimpleTaskScheduler::Create(clock);
+
+  // Schedule a task to fire immediately. The counter is incremented, but the
+  // 1st execution is still running.
+  scheduler->ScheduleAt(kTaskId, task, clock.GetSystemTimeMilliseconds());
+  std::this_thread::sleep_for(std::chrono::milliseconds(kSleepMsEps));
+  EXPECT_THAT(counter, Eq(1));
+
+  // Schedule the task for the 2nd time to fire in 10 ms.
+  scheduler->ScheduleAt(kTaskId, task, clock.GetSystemTimeMilliseconds() + 10);
+  // Sleep for 10 ms. The 1st execution is still running, so the next execution
+  // should wait and execute after the 1st execution finishes.
+  std::this_thread::sleep_for(std::chrono::milliseconds(10 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(1));
+
+  // Schedule the task for the 3rd time to fire in 20 ms.
+  scheduler->ScheduleAt(kTaskId, task, clock.GetSystemTimeMilliseconds() + 20);
+  // Sleep for 10 ms. The 1st execution is still running, so the next execution
+  // should wait and execute after the 1st execution finishes.
+  std::this_thread::sleep_for(std::chrono::milliseconds(10 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(1));
+
+  // Finally, the 1st execution finishes and another execution fires.
+  std::this_thread::sleep_for(std::chrono::milliseconds(2000 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(2));
+
+  // Sleep for another 2 seconds. The counter should remain 2 since only one
+  // execution should fire after the 1st execution finishes, even though there
+  // were two executions expected to fire when the 1st execution was still
+  // running.
+  std::this_thread::sleep_for(std::chrono::milliseconds(2000 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(2));
+}
+
+TEST(SimpleTaskSchedulerTest, MockClock) {
+  FakeClock fake_clock;
+  std::atomic<int> counter = 0;
+  const auto task = [&counter]() { ++counter; };
+
+  std::unique_ptr<SimpleTaskScheduler> scheduler =
+      SimpleTaskScheduler::Create(fake_clock);
+
+  fake_clock.SetSystemTimeMilliseconds(30000);
+  // Schedule a task to fire in 1 second.
+  scheduler->ScheduleAt(kTaskId, task,
+                        fake_clock.GetSystemTimeMilliseconds() + 1000);
+
+  // After 1 second, the task should fire and increment the counter.
+  std::this_thread::sleep_for(std::chrono::milliseconds(1000 + kSleepMsEps));
+  EXPECT_THAT(counter, Eq(1));
+}
+
+TEST(SimpleTaskSchedulerTest, SelfTask) {
+  Clock clock;
+
+  auto main_class = std::make_unique<MainClass>(clock);
+  main_class->Initialize();
+
+  // After 1 second, the task should fire and increment the counter.
+  std::this_thread::sleep_for(std::chrono::milliseconds(1000 + kSleepMsEps));
+  EXPECT_THAT(main_class->GetCounter(), Eq(1));
+
+  // After another 1 second, the task should fire and increment the counter.
+  std::this_thread::sleep_for(std::chrono::milliseconds(1000 + kSleepMsEps));
+  EXPECT_THAT(main_class->GetCounter(), Eq(2));
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/util/timestamp-util.cc b/icing/util/timestamp-util.cc
new file mode 100644
index 0000000..6e50d2f
--- /dev/null
+++ b/icing/util/timestamp-util.cc
@@ -0,0 +1,49 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/util/timestamp-util.h"
+
+#include <cstdint>
+#include <limits>
+
+namespace icing {
+namespace lib {
+
+namespace timestamp_util {
+
+int64_t CalculateRawExpirationTimestampMs(int64_t creation_timestamp_ms,
+                                          int64_t ttl_ms) {
+  if (ttl_ms == 0) {
+    // Special case where a TTL of 0 indicates the document should never
+    // expire. int64_t max, interpreted as seconds since epoch, represents
+    // some point in the year 292,277,026,596. So we're probably ok to use
+    // this as "never reaching this point".
+    return std::numeric_limits<int64_t>::max();
+  }
+
+  int64_t expiration_timestamp_ms;
+  if (__builtin_add_overflow(creation_timestamp_ms, ttl_ms,
+                             &expiration_timestamp_ms)) {
+    // Overflow detected. Treat overflow as the same behavior of just int64_t
+    // max
+    return std::numeric_limits<int64_t>::max();
+  }
+
+  return expiration_timestamp_ms;
+}
+
+}  // namespace timestamp_util
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/util/timestamp-util.h b/icing/util/timestamp-util.h
new file mode 100644
index 0000000..6479b4f
--- /dev/null
+++ b/icing/util/timestamp-util.h
@@ -0,0 +1,42 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef ICING_UTIL_TIMESTAMP_UTIL_H_
+#define ICING_UTIL_TIMESTAMP_UTIL_H_
+
+#include <cstdint>
+
+namespace icing {
+namespace lib {
+
+namespace timestamp_util {
+
+// Calculates the (raw) expiration timestamp of a document given its creation
+// timestamp and time-to-live (TTL) in milliseconds.
+//
+// If the TTL is 0, the document should never expire and the function will
+// return INT64_MAX as the expiration timestamp.
+//
+// If an overflow occurs, the function will return INT64_MAX.
+//
+// REQUIRES: creation_timestamp_ms >= 0 && ttl_ms >= 0.
+int64_t CalculateRawExpirationTimestampMs(int64_t creation_timestamp_ms,
+                                          int64_t ttl_ms);
+
+}  // namespace timestamp_util
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_UTIL_TIMESTAMP_UTIL_H_
diff --git a/icing/util/timestamp-util_test.cc b/icing/util/timestamp-util_test.cc
new file mode 100644
index 0000000..fd488cd
--- /dev/null
+++ b/icing/util/timestamp-util_test.cc
@@ -0,0 +1,88 @@
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "icing/util/timestamp-util.h"
+
+#include <cstdint>
+#include <limits>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace icing {
+namespace lib {
+namespace timestamp_util {
+
+namespace {
+
+using ::testing::Eq;
+
+TEST(TimestampUtilTest, CalculateRawExpirationTimestampMs) {
+  EXPECT_THAT(CalculateRawExpirationTimestampMs(/*creation_timestamp_ms=*/0,
+                                                /*ttl_ms=*/1000),
+              Eq(1000));
+  EXPECT_THAT(CalculateRawExpirationTimestampMs(/*creation_timestamp_ms=*/1000,
+                                                /*ttl_ms=*/1000),
+              Eq(2000));
+  EXPECT_THAT(CalculateRawExpirationTimestampMs(/*creation_timestamp_ms=*/2000,
+                                                /*ttl_ms=*/1000),
+              Eq(3000));
+  EXPECT_THAT(CalculateRawExpirationTimestampMs(/*creation_timestamp_ms=*/5000,
+                                                /*ttl_ms=*/1000),
+              Eq(6000));
+  EXPECT_THAT(CalculateRawExpirationTimestampMs(/*creation_timestamp_ms=*/5000,
+                                                /*ttl_ms=*/3000),
+              Eq(8000));
+  EXPECT_THAT(CalculateRawExpirationTimestampMs(/*creation_timestamp_ms=*/5000,
+                                                /*ttl_ms=*/10000),
+              Eq(15000));
+}
+
+TEST(TimestampUtilTest,
+     CalculateRawExpirationTimestampMs_zeroTtlShouldReturnInt64Max) {
+  EXPECT_THAT(CalculateRawExpirationTimestampMs(/*creation_timestamp_ms=*/1000,
+                                                /*ttl_ms=*/0),
+              Eq(std::numeric_limits<int64_t>::max()));
+  EXPECT_THAT(CalculateRawExpirationTimestampMs(/*creation_timestamp_ms=*/2000,
+                                                /*ttl_ms=*/0),
+              Eq(std::numeric_limits<int64_t>::max()));
+  EXPECT_THAT(CalculateRawExpirationTimestampMs(/*creation_timestamp_ms=*/5000,
+                                                /*ttl_ms=*/0),
+              Eq(std::numeric_limits<int64_t>::max()));
+}
+
+TEST(TimestampUtilTest,
+     CalculateRawExpirationTimestampMs_shouldPreventOverflow) {
+  EXPECT_THAT(
+      CalculateRawExpirationTimestampMs(
+          /*creation_timestamp_ms=*/std::numeric_limits<int64_t>::max() - 2,
+          /*ttl_ms=*/2),
+      Eq(std::numeric_limits<int64_t>::max()));
+  EXPECT_THAT(
+      CalculateRawExpirationTimestampMs(
+          /*creation_timestamp_ms=*/std::numeric_limits<int64_t>::max() - 2,
+          /*ttl_ms=*/100),
+      Eq(std::numeric_limits<int64_t>::max()));
+  EXPECT_THAT(
+      CalculateRawExpirationTimestampMs(
+          /*creation_timestamp_ms=*/std::numeric_limits<int64_t>::max() - 1,
+          /*ttl_ms=*/100000),
+      Eq(std::numeric_limits<int64_t>::max()));
+}
+
+}  // namespace
+
+}  // namespace timestamp_util
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/util/tokenized-document.cc b/icing/util/tokenized-document.cc
index 576e0a2..61f4396 100644
--- a/icing/util/tokenized-document.cc
+++ b/icing/util/tokenized-document.cc
@@ -14,6 +14,7 @@
 
 #include "icing/util/tokenized-document.h"
 
+#include <cstdint>
 #include <memory>
 #include <string_view>
 #include <utility>
@@ -21,6 +22,7 @@
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/proto/document.pb.h"
+#include "icing/proto/document_wrapper.pb.h"
 #include "icing/schema/joinable-property.h"
 #include "icing/schema/schema-store.h"
 #include "icing/schema/section.h"
@@ -28,6 +30,7 @@
 #include "icing/tokenization/token.h"
 #include "icing/tokenization/tokenizer-factory.h"
 #include "icing/tokenization/tokenizer.h"
+#include "icing/util/document-util.h"
 #include "icing/util/document-validator.h"
 #include "icing/util/status-macros.h"
 
@@ -41,6 +44,7 @@
     const LanguageSegmenter* language_segmenter,
     const std::vector<Section<std::string_view>>& string_sections) {
   std::vector<TokenizedSection> tokenized_string_sections;
+  std::vector<Token> batch_tokens;
   for (const Section<std::string_view>& section : string_sections) {
     ICING_ASSIGN_OR_RETURN(std::unique_ptr<Tokenizer> tokenizer,
                            tokenizer_factory::CreateIndexingTokenizer(
@@ -50,7 +54,7 @@
       ICING_ASSIGN_OR_RETURN(std::unique_ptr<Tokenizer::Iterator> itr,
                              tokenizer->Tokenize(subcontent));
       while (itr->Advance()) {
-        std::vector<Token> batch_tokens = itr->GetTokens();
+        itr->GetTokens(&batch_tokens);
         for (const Token& token : batch_tokens) {
           token_sequence.push_back(token.text);
         }
@@ -68,21 +72,28 @@
 /* static */ libtextclassifier3::StatusOr<TokenizedDocument>
 TokenizedDocument::Create(const SchemaStore* schema_store,
                           const LanguageSegmenter* language_segmenter,
-                          DocumentProto document) {
+                          int64_t current_time_ms, DocumentProto document) {
+  // Set the creation timestamp if it is not set.
+  if (document.creation_timestamp_ms() == 0) {
+    document.set_creation_timestamp_ms(current_time_ms);
+  }
+
   // Since there are many std::string_view objects pointing to the document
-  // proto, we should make sure DocumentProto has a fixed address. The simplest
-  // way is to use a unique_ptr.
-  auto document_ptr = std::make_unique<DocumentProto>(std::move(document));
+  // proto, we should make sure DocumentProto in DocumentWrapper has a fixed
+  // address. The simplest way is to use a unique_ptr.
+  auto document_wrapper_ptr = std::make_unique<DocumentWrapper>(
+      document_util::CreateDocumentWrapper(std::move(document)));
 
   DocumentValidator validator(schema_store);
-  ICING_RETURN_IF_ERROR(validator.Validate(*document_ptr));
-
-  ICING_ASSIGN_OR_RETURN(SectionGroup section_group,
-                         schema_store->ExtractSections(*document_ptr));
+  ICING_RETURN_IF_ERROR(validator.Validate(document_wrapper_ptr->document()));
 
   ICING_ASSIGN_OR_RETURN(
-      JoinablePropertyGroup joinable_property_group,
-      schema_store->ExtractJoinableProperties(*document_ptr));
+      SectionGroup section_group,
+      schema_store->ExtractSections(document_wrapper_ptr->document()));
+
+  ICING_ASSIGN_OR_RETURN(JoinablePropertyGroup joinable_property_group,
+                         schema_store->ExtractJoinableProperties(
+                             document_wrapper_ptr->document()));
 
   // Tokenize string sections
   ICING_ASSIGN_OR_RETURN(
@@ -90,11 +101,19 @@
       Tokenize(schema_store, language_segmenter,
                section_group.string_sections));
 
-  return TokenizedDocument(std::move(document_ptr),
-                           std::move(tokenized_string_sections),
-                           std::move(section_group.integer_sections),
-                           std::move(section_group.vector_sections),
-                           std::move(joinable_property_group));
+  TokenizedDocument tokenized_document(
+      std::move(document_wrapper_ptr), std::move(tokenized_string_sections),
+      std::move(section_group.integer_sections),
+      std::move(section_group.vector_sections),
+      std::move(joinable_property_group));
+
+  // Set the num_string_tokens into the document proto.
+  int32_t num_string_tokens = tokenized_document.num_string_tokens();
+  tokenized_document.document_wrapper_->mutable_document()
+      ->mutable_internal_fields()
+      ->set_length_in_tokens(num_string_tokens);
+
+  return tokenized_document;
 }
 
 }  // namespace lib
diff --git a/icing/util/tokenized-document.h b/icing/util/tokenized-document.h
index ae92311..5a7f635 100644
--- a/icing/util/tokenized-document.h
+++ b/icing/util/tokenized-document.h
@@ -23,6 +23,7 @@
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/proto/document.pb.h"
+#include "icing/proto/document_wrapper.pb.h"
 #include "icing/schema/joinable-property.h"
 #include "icing/schema/schema-store.h"
 #include "icing/schema/section.h"
@@ -41,13 +42,29 @@
         token_sequence(std::move(token_sequence_in)) {}
 };
 
+// A wrapper class to hold the input DocumentProto and the tokenized sections.
+// It finalizes the proto (to be added into DocumentStore) and tokens (to be
+// indexed), so after the creation, the proto and tokens are immutable and the
+// caller can simply use them to write.
+//
+// Note: the document dependency is not the responsibility of this class.
+// Additional steps (see DocumentDependencyProcessor) are needed to handle the
+// dependencies.
+//
+// Some essential fields are populated into the DocumentProto. For example:
+// - Creation timestamp: if it is not set by the client, it will be set to the
+//   current time.
+// - Length in tokens: the number of tokens in all string sections.
 class TokenizedDocument {
  public:
   static libtextclassifier3::StatusOr<TokenizedDocument> Create(
       const SchemaStore* schema_store,
-      const LanguageSegmenter* language_segmenter, DocumentProto document);
+      const LanguageSegmenter* language_segmenter, int64_t current_time_ms,
+      DocumentProto document);
 
-  const DocumentProto& document() const { return *document_; }
+  // Due to DocumentStore's internal implementation, we need to wrap
+  // DocumentProto into DocumentWrapper.
+  const DocumentWrapper& document_wrapper() const { return *document_wrapper_; }
 
   int32_t num_string_tokens() const {
     int32_t num_string_tokens = 0;
@@ -78,18 +95,18 @@
  private:
   // Use TokenizedDocument::Create() to instantiate.
   explicit TokenizedDocument(
-      std::unique_ptr<DocumentProto> document,
+      std::unique_ptr<DocumentWrapper> document_wrapper,
       std::vector<TokenizedSection>&& tokenized_string_sections,
       std::vector<Section<int64_t>>&& integer_sections,
       std::vector<Section<PropertyProto::VectorProto>>&& vector_sections,
       JoinablePropertyGroup&& joinable_property_group)
-      : document_(std::move(document)),
+      : document_wrapper_(std::move(document_wrapper)),
         tokenized_string_sections_(std::move(tokenized_string_sections)),
         integer_sections_(std::move(integer_sections)),
         vector_sections_(std::move(vector_sections)),
         joinable_property_group_(std::move(joinable_property_group)) {}
 
-  std::unique_ptr<DocumentProto> document_;
+  std::unique_ptr<DocumentWrapper> document_wrapper_;
   std::vector<TokenizedSection> tokenized_string_sections_;
   std::vector<Section<int64_t>> integer_sections_;
   std::vector<Section<PropertyProto::VectorProto>> vector_sections_;
diff --git a/icing/util/tokenized-document_test.cc b/icing/util/tokenized-document_test.cc
index 513b8d0..84528e7 100644
--- a/icing/util/tokenized-document_test.cc
+++ b/icing/util/tokenized-document_test.cc
@@ -14,6 +14,7 @@
 
 #include "icing/util/tokenized-document.h"
 
+#include <cstdint>
 #include <memory>
 #include <string>
 #include <string_view>
@@ -27,6 +28,7 @@
 #include "icing/file/filesystem.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document.pb.h"
+#include "icing/proto/document_wrapper.pb.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/schema-builder.h"
 #include "icing/schema/joinable-property.h"
@@ -39,6 +41,7 @@
 #include "icing/testing/tmp-directory.h"
 #include "icing/tokenization/language-segmenter-factory.h"
 #include "icing/tokenization/language-segmenter.h"
+#include "icing/util/document-util.h"
 #include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
@@ -239,7 +242,15 @@
   std::unique_ptr<SchemaStore> schema_store_;
 };
 
+DocumentWrapper CreateDocumentWrapper(DocumentProto document,
+                                      int32_t num_string_tokens) {
+  document.mutable_internal_fields()->set_length_in_tokens(num_string_tokens);
+  return document_util::CreateDocumentWrapper(std::move(document));
+}
+
 TEST_F(TokenizedDocumentTest, CreateAll) {
+  int64_t current_time_ms = 123;
+
   PropertyProto::VectorProto vector1;
   vector1.set_model_signature("my_model1");
   vector1.add_values(1.0f);
@@ -252,6 +263,7 @@
 
   DocumentProto document =
       DocumentBuilder()
+          .SetCreationTimestampMs(current_time_ms)
           .SetKey("icing", "fake_type/1")
           .SetSchema(std::string(kFakeType))
           .AddStringProperty(std::string(kUnindexedStringProperty),
@@ -273,9 +285,11 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+                                current_time_ms, document));
 
-  EXPECT_THAT(tokenized_document.document(), EqualsProto(document));
+  EXPECT_THAT(
+      tokenized_document.document_wrapper(),
+      EqualsProto(CreateDocumentWrapper(document, /*num_string_tokens=*/9)));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(9));
 
   // string sections
@@ -326,8 +340,11 @@
 }
 
 TEST_F(TokenizedDocumentTest, CreateNoIndexableIntegerProperties) {
+  int64_t current_time_ms = 123;
+
   DocumentProto document =
       DocumentBuilder()
+          .SetCreationTimestampMs(current_time_ms)
           .SetKey("icing", "fake_type/1")
           .SetSchema(std::string(kFakeType))
           .AddInt64Property(std::string(kUnindexedIntegerProperty), 789)
@@ -336,9 +353,11 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+                                current_time_ms, document));
 
-  EXPECT_THAT(tokenized_document.document(), EqualsProto(document));
+  EXPECT_THAT(
+      tokenized_document.document_wrapper(),
+      EqualsProto(CreateDocumentWrapper(document, /*num_string_tokens=*/0)));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(0));
 
   // string sections
@@ -355,8 +374,11 @@
 }
 
 TEST_F(TokenizedDocumentTest, CreateMultipleIndexableIntegerProperties) {
+  int64_t current_time_ms = 123;
+
   DocumentProto document =
       DocumentBuilder()
+          .SetCreationTimestampMs(current_time_ms)
           .SetKey("icing", "fake_type/1")
           .SetSchema(std::string(kFakeType))
           .AddInt64Property(std::string(kUnindexedIntegerProperty), 789)
@@ -367,9 +389,11 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+                                current_time_ms, document));
 
-  EXPECT_THAT(tokenized_document.document(), EqualsProto(document));
+  EXPECT_THAT(
+      tokenized_document.document_wrapper(),
+      EqualsProto(CreateDocumentWrapper(document, /*num_string_tokens=*/0)));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(0));
 
   // string sections
@@ -394,8 +418,11 @@
 }
 
 TEST_F(TokenizedDocumentTest, CreateNoIndexableStringProperties) {
+  int64_t current_time_ms = 123;
+
   DocumentProto document =
       DocumentBuilder()
+          .SetCreationTimestampMs(current_time_ms)
           .SetKey("icing", "fake_type/1")
           .SetSchema(std::string(kFakeType))
           .AddStringProperty(std::string(kUnindexedStringProperty),
@@ -405,9 +432,11 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+                                current_time_ms, document));
 
-  EXPECT_THAT(tokenized_document.document(), EqualsProto(document));
+  EXPECT_THAT(
+      tokenized_document.document_wrapper(),
+      EqualsProto(CreateDocumentWrapper(document, /*num_string_tokens=*/0)));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(0));
 
   // string sections
@@ -424,8 +453,11 @@
 }
 
 TEST_F(TokenizedDocumentTest, CreateMultipleIndexableStringProperties) {
+  int64_t current_time_ms = 123;
+
   DocumentProto document =
       DocumentBuilder()
+          .SetCreationTimestampMs(current_time_ms)
           .SetKey("icing", "fake_type/1")
           .SetSchema(std::string(kFakeType))
           .AddStringProperty(std::string(kUnindexedStringProperty),
@@ -438,9 +470,11 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+                                current_time_ms, document));
 
-  EXPECT_THAT(tokenized_document.document(), EqualsProto(document));
+  EXPECT_THAT(
+      tokenized_document.document_wrapper(),
+      EqualsProto(CreateDocumentWrapper(document, /*num_string_tokens=*/9)));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(9));
 
   // string sections
@@ -467,12 +501,15 @@
 }
 
 TEST_F(TokenizedDocumentTest, CreateNoIndexableVectorProperties) {
+  int64_t current_time_ms = 123;
+
   PropertyProto::VectorProto vector;
   vector.set_model_signature("my_model");
   vector.add_values(1.0f);
 
   DocumentProto document =
       DocumentBuilder()
+          .SetCreationTimestampMs(current_time_ms)
           .SetKey("icing", "fake_type/1")
           .SetSchema(std::string(kFakeType))
           .AddVectorProperty(std::string(kUnindexedVectorProperty), vector)
@@ -481,9 +518,11 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+                                current_time_ms, document));
 
-  EXPECT_THAT(tokenized_document.document(), EqualsProto(document));
+  EXPECT_THAT(
+      tokenized_document.document_wrapper(),
+      EqualsProto(CreateDocumentWrapper(document, /*num_string_tokens=*/0)));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(0));
 
   // string sections
@@ -500,6 +539,8 @@
 }
 
 TEST_F(TokenizedDocumentTest, CreateMultipleIndexableVectorProperties) {
+  int64_t current_time_ms = 123;
+
   PropertyProto::VectorProto vector1;
   vector1.set_model_signature("my_model1");
   vector1.add_values(1.0f);
@@ -512,6 +553,7 @@
 
   DocumentProto document =
       DocumentBuilder()
+          .SetCreationTimestampMs(current_time_ms)
           .SetKey("icing", "fake_type/1")
           .SetSchema(std::string(kFakeType))
           .AddVectorProperty(std::string(kUnindexedVectorProperty), vector1)
@@ -523,9 +565,11 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+                                current_time_ms, document));
 
-  EXPECT_THAT(tokenized_document.document(), EqualsProto(document));
+  EXPECT_THAT(
+      tokenized_document.document_wrapper(),
+      EqualsProto(CreateDocumentWrapper(document, /*num_string_tokens=*/0)));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(0));
 
   // string sections
@@ -550,8 +594,11 @@
 }
 
 TEST_F(TokenizedDocumentTest, CreateNoJoinQualifiedIdProperties) {
+  int64_t current_time_ms = 123;
+
   DocumentProto document =
       DocumentBuilder()
+          .SetCreationTimestampMs(current_time_ms)
           .SetKey("icing", "fake_type/1")
           .SetSchema(std::string(kFakeType))
           .AddStringProperty(std::string(kUnindexedStringProperty),
@@ -561,9 +608,11 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+                                current_time_ms, document));
 
-  EXPECT_THAT(tokenized_document.document(), EqualsProto(document));
+  EXPECT_THAT(
+      tokenized_document.document_wrapper(),
+      EqualsProto(CreateDocumentWrapper(document, /*num_string_tokens=*/0)));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(0));
 
   // string sections
@@ -580,8 +629,11 @@
 }
 
 TEST_F(TokenizedDocumentTest, CreateMultipleJoinQualifiedIdProperties) {
+  int64_t current_time_ms = 123;
+
   DocumentProto document =
       DocumentBuilder()
+          .SetCreationTimestampMs(current_time_ms)
           .SetKey("icing", "fake_type/1")
           .SetSchema(std::string(kFakeType))
           .AddStringProperty(std::string(kUnindexedStringProperty),
@@ -593,9 +645,11 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
-                                document));
+                                current_time_ms, document));
 
-  EXPECT_THAT(tokenized_document.document(), EqualsProto(document));
+  EXPECT_THAT(
+      tokenized_document.document_wrapper(),
+      EqualsProto(CreateDocumentWrapper(document, /*num_string_tokens=*/0)));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(0));
 
   // string sections
@@ -619,6 +673,40 @@
               ElementsAre("pkg$db/ns#uri2"));
 }
 
+TEST_F(TokenizedDocumentTest,
+       CreateShouldSetCreationTimestampToCurrentTimeIfUnset) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("icing", "fake_type/1")
+                               .SetSchema(std::string(kFakeType))
+                               .Build();
+  ASSERT_THAT(document.creation_timestamp_ms(), Eq(0));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_document,
+      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
+                                /*current_time_ms=*/123, document));
+  EXPECT_THAT(
+      tokenized_document.document_wrapper().document().creation_timestamp_ms(),
+      Eq(123));
+}
+
+TEST_F(TokenizedDocumentTest,
+       CreateShouldNotOverwriteExistingCreationTimestamp) {
+  DocumentProto document = DocumentBuilder()
+                               .SetCreationTimestampMs(123)
+                               .SetKey("icing", "fake_type/1")
+                               .SetSchema(std::string(kFakeType))
+                               .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_document,
+      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
+                                /*current_time_ms=*/456, document));
+  EXPECT_THAT(
+      tokenized_document.document_wrapper().document().creation_timestamp_ms(),
+      Eq(123));
+}
+
 }  // namespace
 
 }  // namespace lib
diff --git a/java/src/com/google/android/icing/IcingSearchEngine.java b/java/src/com/google/android/icing/IcingSearchEngine.java
index 9856e09..f44a1f7 100644
--- a/java/src/com/google/android/icing/IcingSearchEngine.java
+++ b/java/src/com/google/android/icing/IcingSearchEngine.java
@@ -14,6 +14,7 @@
 
 package com.google.android.icing;
 
+import com.google.android.icing.proto.BatchGetResultProto;
 import com.google.android.icing.proto.BatchPutResultProto;
 import com.google.android.icing.proto.BlobProto;
 import com.google.android.icing.proto.DebugInfoResultProto;
@@ -24,11 +25,13 @@
 import com.google.android.icing.proto.DeleteResultProto;
 import com.google.android.icing.proto.DocumentProto;
 import com.google.android.icing.proto.GetAllNamespacesResultProto;
+import com.google.android.icing.proto.GetNextPageRequestProto;
 import com.google.android.icing.proto.GetOptimizeInfoResultProto;
 import com.google.android.icing.proto.GetResultProto;
 import com.google.android.icing.proto.GetResultSpecProto;
 import com.google.android.icing.proto.GetSchemaResultProto;
 import com.google.android.icing.proto.GetSchemaTypeResultProto;
+import com.google.android.icing.proto.HandleExpiredDocumentsResultProto;
 import com.google.android.icing.proto.IcingSearchEngineOptions;
 import com.google.android.icing.proto.InitializeResultProto;
 import com.google.android.icing.proto.LogSeverity;
@@ -135,7 +138,7 @@
 
   @Override
   public @NonNull BatchPutResultProto batchPut(@NonNull PutDocumentRequest documents) {
-    return IcingSearchEngineUtils.byteArrayToPutResultProtos(
+    return IcingSearchEngineUtils.byteArrayToBatchPutResultProto(
         icingSearchEngineImpl.batchPut(documents.toByteArray()));
   }
 
@@ -147,6 +150,12 @@
   }
 
   @Override
+  public @NonNull BatchGetResultProto batchGet(@NonNull GetResultSpecProto getResultSpec) {
+    return IcingSearchEngineUtils.byteArrayToBatchGetResultProto(
+        icingSearchEngineImpl.batchGet(getResultSpec.toByteArray()));
+  }
+
+  @Override
   public @NonNull ReportUsageResultProto reportUsage(@NonNull UsageReport usageReport) {
     return IcingSearchEngineUtils.byteArrayToReportUsageResultProto(
         icingSearchEngineImpl.reportUsage(usageReport.toByteArray()));
@@ -175,11 +184,24 @@
   }
 
   @Override
+  public @NonNull SearchResultProto getNextPage(
+      @NonNull GetNextPageRequestProto getNextPageRequest) {
+    return IcingSearchEngineUtils.byteArrayToSearchResultProto(
+        icingSearchEngineImpl.getNextPageWithRequestProto(getNextPageRequest.toByteArray()));
+  }
+
+  @Override
   public void invalidateNextPageToken(long nextPageToken) {
     icingSearchEngineImpl.invalidateNextPageToken(nextPageToken);
   }
 
   @Override
+  public @NonNull HandleExpiredDocumentsResultProto handleExpiredDocuments() {
+    return IcingSearchEngineUtils.byteArrayToHandleExpiredDocumentsResultProto(
+        icingSearchEngineImpl.handleExpiredDocuments());
+  }
+
+  @Override
   public @NonNull BlobProto openWriteBlob(PropertyProto.@NonNull BlobHandleProto blobHandle) {
     return IcingSearchEngineUtils.byteArrayToBlobProto(
         icingSearchEngineImpl.openWriteBlob(blobHandle.toByteArray()));
@@ -204,6 +226,17 @@
   }
 
   @Override
+  public @NonNull BlobProto getAllBlobInfos() {
+    return IcingSearchEngineUtils.byteArrayToBlobProto(icingSearchEngineImpl.getAllBlobInfos());
+  }
+
+  @Override
+  public @NonNull BlobProto putBlobInfos(@NonNull BlobProto blobProto) {
+    return IcingSearchEngineUtils.byteArrayToBlobProto(
+        icingSearchEngineImpl.putBlobInfos(blobProto.toByteArray()));
+  }
+
+  @Override
   public @NonNull DeleteResultProto delete(@NonNull String namespace, @NonNull String uri) {
     return IcingSearchEngineUtils.byteArrayToDeleteResultProto(
         icingSearchEngineImpl.delete(namespace, uri));
@@ -275,6 +308,12 @@
     return IcingSearchEngineUtils.byteArrayToResetResultProto(icingSearchEngineImpl.reset());
   }
 
+  @Override
+  public @NonNull ResetResultProto clearAndDestroy() {
+    return IcingSearchEngineUtils.byteArrayToResetResultProto(
+        icingSearchEngineImpl.clearAndDestroy());
+  }
+
   public static boolean shouldLog(LogSeverity.Code severity) {
     return shouldLog(severity, (short) 0);
   }
diff --git a/java/src/com/google/android/icing/IcingSearchEngineImpl.java b/java/src/com/google/android/icing/IcingSearchEngineImpl.java
index ad5a5aa..0389542 100644
--- a/java/src/com/google/android/icing/IcingSearchEngineImpl.java
+++ b/java/src/com/google/android/icing/IcingSearchEngineImpl.java
@@ -132,6 +132,12 @@
   }
 
   @Nullable
+  public byte[] batchGet(@NonNull byte[] getResultSpecBytes) {
+    throwIfClosed();
+    return nativeBatchGet(this, getResultSpecBytes);
+  }
+
+  @Nullable
   public byte[] reportUsage(@NonNull byte[] usageReportBytes) {
     throwIfClosed();
     return nativeReportUsage(this, usageReportBytes);
@@ -168,11 +174,24 @@
     return nativeGetNextPage(this, nextPageToken, System.currentTimeMillis());
   }
 
+  @Nullable
+  public byte[] getNextPageWithRequestProto(@NonNull byte[] getNextPageRequestBytes) {
+    throwIfClosed();
+    return nativeGetNextPageWithRequestProto(
+        this, getNextPageRequestBytes, System.currentTimeMillis());
+  }
+
   public void invalidateNextPageToken(long nextPageToken) {
     throwIfClosed();
     nativeInvalidateNextPageToken(this, nextPageToken);
   }
 
+  @Nullable
+  public byte[] handleExpiredDocuments() {
+    throwIfClosed();
+    return nativeHandleExpiredDocuments(this);
+  }
+
   @NonNull
   public byte[] openWriteBlob(@NonNull byte[] blobHandleBytes) {
     throwIfClosed();
@@ -197,6 +216,18 @@
     return nativeCommitBlob(this, blobHandleBytes);
   }
 
+  @NonNull
+  public byte[] getAllBlobInfos() {
+    throwIfClosed();
+    return nativeGetAllBlobInfos(this);
+  }
+
+  @NonNull
+  public byte[] putBlobInfos(@NonNull byte[] blobProtoBytes) {
+    throwIfClosed();
+    return nativePutBlobInfos(this, blobProtoBytes);
+  }
+
   @Nullable
   public byte[] delete(@NonNull String namespace, @NonNull String uri) {
     throwIfClosed();
@@ -268,6 +299,12 @@
     return nativeReset(this);
   }
 
+  @Nullable
+  public byte[] clearAndDestroy() {
+    throwIfClosed();
+    return nativeClearAndDestroy(this);
+  }
+
   public static boolean shouldLog(short severity) {
     return shouldLog(severity, (short) 0);
   }
@@ -321,6 +358,9 @@
   private static native byte[] nativeGet(
       IcingSearchEngineImpl instance, String namespace, String uri, byte[] getResultSpecBytes);
 
+  private static native byte[] nativeBatchGet(
+      IcingSearchEngineImpl instance, byte[] getResultSpecBytes);
+
   private static native byte[] nativeReportUsage(
       IcingSearchEngineImpl instance, byte[] usageReportBytes);
 
@@ -336,9 +376,16 @@
   private static native byte[] nativeGetNextPage(
       IcingSearchEngineImpl instance, long nextPageToken, long javaToNativeStartTimestampMs);
 
+  private static native byte[] nativeGetNextPageWithRequestProto(
+      IcingSearchEngineImpl instance,
+      byte[] getNextPageRequestBytes,
+      long javaToNativeStartTimestampMs);
+
   private static native void nativeInvalidateNextPageToken(
       IcingSearchEngineImpl instance, long nextPageToken);
 
+  private static native byte[] nativeHandleExpiredDocuments(IcingSearchEngineImpl instance);
+
   private static native byte[] nativeOpenWriteBlob(
       IcingSearchEngineImpl instance, byte[] blobHandleBytes);
 
@@ -351,6 +398,11 @@
   private static native byte[] nativeCommitBlob(
       IcingSearchEngineImpl instance, byte[] blobHandleBytes);
 
+  private static native byte[] nativeGetAllBlobInfos(IcingSearchEngineImpl instance);
+
+  private static native byte[] nativePutBlobInfos(
+      IcingSearchEngineImpl instance, byte[] blobProtoBytes);
+
   private static native byte[] nativeDelete(
       IcingSearchEngineImpl instance, String namespace, String uri);
 
@@ -373,6 +425,8 @@
 
   private static native byte[] nativeReset(IcingSearchEngineImpl instance);
 
+  private static native byte[] nativeClearAndDestroy(IcingSearchEngineImpl instance);
+
   private static native byte[] nativeSearchSuggestions(
       IcingSearchEngineImpl instance, byte[] suggestionSpecBytes);
 
diff --git a/java/src/com/google/android/icing/IcingSearchEngineInterface.java b/java/src/com/google/android/icing/IcingSearchEngineInterface.java
index c381d7c..bf6ba10 100644
--- a/java/src/com/google/android/icing/IcingSearchEngineInterface.java
+++ b/java/src/com/google/android/icing/IcingSearchEngineInterface.java
@@ -1,5 +1,6 @@
 package com.google.android.icing;
 
+import com.google.android.icing.proto.BatchGetResultProto;
 import com.google.android.icing.proto.BatchPutResultProto;
 import com.google.android.icing.proto.BlobProto;
 import com.google.android.icing.proto.DebugInfoResultProto;
@@ -10,11 +11,13 @@
 import com.google.android.icing.proto.DeleteResultProto;
 import com.google.android.icing.proto.DocumentProto;
 import com.google.android.icing.proto.GetAllNamespacesResultProto;
+import com.google.android.icing.proto.GetNextPageRequestProto;
 import com.google.android.icing.proto.GetOptimizeInfoResultProto;
 import com.google.android.icing.proto.GetResultProto;
 import com.google.android.icing.proto.GetResultSpecProto;
 import com.google.android.icing.proto.GetSchemaResultProto;
 import com.google.android.icing.proto.GetSchemaTypeResultProto;
+import com.google.android.icing.proto.HandleExpiredDocumentsResultProto;
 import com.google.android.icing.proto.InitializeResultProto;
 import com.google.android.icing.proto.OptimizeResultProto;
 import com.google.android.icing.proto.PersistToDiskResultProto;
@@ -105,6 +108,13 @@
    */
   GetResultProto get(String namespace, String uri, GetResultSpecProto getResultSpec);
 
+  /**
+   * Gets a list of documents.
+   *
+   * @param getResultSpec the spec for getting the documents.
+   */
+  BatchGetResultProto batchGet(GetResultSpecProto getResultSpec);
+
   /** Reports usage. */
   ReportUsageResultProto reportUsage(UsageReport usageReport);
 
@@ -120,12 +130,46 @@
   SearchResultProto search(
       SearchSpecProto searchSpec, ScoringSpecProto scoringSpec, ResultSpecProto resultSpec);
 
-  /** Gets the next page. */
+  /**
+   * Gets the next page.
+   *
+   * <p>Note: This method is deprecated. Please use {@link #getNextPage(GetNextPageRequestProto)}
+   * instead.
+   */
   SearchResultProto getNextPage(long nextPageToken);
 
+  /**
+   * Gets the next page.
+   *
+   * @param getNextPageRequest the request proto for getting the next page.
+   */
+  SearchResultProto getNextPage(GetNextPageRequestProto getNextPageRequest);
+
   /** Invalidates the next page token. */
   void invalidateNextPageToken(long nextPageToken);
 
+  /**
+   * Handles expired documents. The operation includes:
+   *
+   * <ul>
+   *   <li>Zero out bytes for all expired documents.
+   *   <li>Propagate delete to child documents with delete propagation enabled. Zero out bytes for
+   *       all propagated documents.
+   * </ul>
+   *
+   * <p>For Icing direct clients, there is no need to call this API since Icing already has a native
+   * background thread for scheduling tasks to call it. This API is exposed only for AppSearch
+   * system service (in Android) since native background thread is not recommended in system
+   * service, and tasks should be scheduled at AppSearch level instead.
+   *
+   * <p>Note: {@link #optimize()} also zeros out bytes for non-alive (delete, expired) documents,
+   * but it also additionally reclaims disk space for storage and rebuilds indices by rearranging
+   * alive documents. On the other hand, {@link #handleExpiredDocuments()} is a cheaper API that
+   * only deals with expired documents and delete propagation for child documents, without
+   * reclaiming disk space or rebuilding indices which requires additional disk I/O.
+   */
+  HandleExpiredDocumentsResultProto handleExpiredDocuments();
+
   /** Gets a file descriptor to write blob data. */
   BlobProto openWriteBlob(PropertyProto.BlobHandleProto blobHandle);
 
@@ -138,6 +182,12 @@
   /** Marks the blob is committed. */
   BlobProto commitBlob(PropertyProto.BlobHandleProto blobHandle);
 
+  /** Gets all the blob info from the blob store. */
+  BlobProto getAllBlobInfos();
+
+  /** Puts the blob info protos from the blob proto to the blob info proto log file. */
+  BlobProto putBlobInfos(BlobProto blobProto);
+
   /**
    * Deletes the document.
    *
@@ -185,6 +235,9 @@
   /** Clears all data from the current icing instance, and reinitializes it. */
   ResetResultProto reset();
 
+  /** Clears all data from the current icing instance. */
+  ResetResultProto clearAndDestroy();
+
   /** Closes the current icing instance. */
   @Override
   void close();
diff --git a/java/src/com/google/android/icing/IcingSearchEngineUtils.java b/java/src/com/google/android/icing/IcingSearchEngineUtils.java
index ca08dd6..1e08262 100644
--- a/java/src/com/google/android/icing/IcingSearchEngineUtils.java
+++ b/java/src/com/google/android/icing/IcingSearchEngineUtils.java
@@ -17,6 +17,7 @@
 import android.util.Log;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import com.google.android.icing.proto.BatchGetResultProto;
 import com.google.android.icing.proto.BatchPutResultProto;
 import com.google.android.icing.proto.BlobProto;
 import com.google.android.icing.proto.DebugInfoResultProto;
@@ -29,6 +30,7 @@
 import com.google.android.icing.proto.GetResultProto;
 import com.google.android.icing.proto.GetSchemaResultProto;
 import com.google.android.icing.proto.GetSchemaTypeResultProto;
+import com.google.android.icing.proto.HandleExpiredDocumentsResultProto;
 import com.google.android.icing.proto.InitializeResultProto;
 import com.google.android.icing.proto.OptimizeResultProto;
 import com.google.android.icing.proto.PersistToDiskResultProto;
@@ -159,18 +161,22 @@
   }
 
   @NonNull
-  public static BatchPutResultProto byteArrayToPutResultProtos(@Nullable byte[] putResultsBytes) {
+  public static BatchPutResultProto byteArrayToBatchPutResultProto(
+      @Nullable byte[] putResultsBytes) {
     if (putResultsBytes == null) {
       Log.e(TAG, "Received null PutResultProtos from native.");
-      // TODO(b/394875109) Or we should create a proto with error status?
-      return BatchPutResultProto.getDefaultInstance();
+      return BatchPutResultProto.newBuilder()
+          .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL))
+          .build();
     }
 
     try {
       return BatchPutResultProto.parseFrom(putResultsBytes, EXTENSION_REGISTRY_LITE);
     } catch (InvalidProtocolBufferException e) {
       Log.e(TAG, "Error parsing PutResultProtos.", e);
-      return BatchPutResultProto.getDefaultInstance();
+      return BatchPutResultProto.newBuilder()
+          .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL))
+          .build();
     }
   }
 
@@ -194,6 +200,26 @@
   }
 
   @NonNull
+  public static BatchGetResultProto byteArrayToBatchGetResultProto(
+      @Nullable byte[] batchGetResultBytes) {
+    if (batchGetResultBytes == null) {
+      Log.e(TAG, "Received null BatchGetResultProto from native.");
+      return BatchGetResultProto.newBuilder()
+          .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL))
+          .build();
+    }
+
+    try {
+      return BatchGetResultProto.parseFrom(batchGetResultBytes, EXTENSION_REGISTRY_LITE);
+    } catch (InvalidProtocolBufferException e) {
+      Log.e(TAG, "Error parsing BatchGetResultProto.", e);
+      return BatchGetResultProto.newBuilder()
+          .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL))
+          .build();
+    }
+  }
+
+  @NonNull
   public static ReportUsageResultProto byteArrayToReportUsageResultProto(
       @Nullable byte[] reportUsageResultBytes) {
     if (reportUsageResultBytes == null) {
@@ -257,6 +283,33 @@
   }
 
   /**
+   * Converts a byte array to a {@link HandleExpiredDocumentsResultProto}.
+   *
+   * @param handleExpiredDocumentsResultBytes the byte array to convert
+   * @return the {@link HandleExpiredDocumentsResultProto}
+   */
+  @NonNull
+  public static HandleExpiredDocumentsResultProto byteArrayToHandleExpiredDocumentsResultProto(
+      @Nullable byte[] handleExpiredDocumentsResultBytes) {
+    if (handleExpiredDocumentsResultBytes == null) {
+      Log.e(TAG, "Received null HandleExpiredDocumentsResultProto from native.");
+      return HandleExpiredDocumentsResultProto.newBuilder()
+          .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL))
+          .build();
+    }
+
+    try {
+      return HandleExpiredDocumentsResultProto.parseFrom(
+          handleExpiredDocumentsResultBytes, EXTENSION_REGISTRY_LITE);
+    } catch (InvalidProtocolBufferException e) {
+      Log.e(TAG, "Error parsing HandleExpiredDocumentsResultProto.", e);
+      return HandleExpiredDocumentsResultProto.newBuilder()
+          .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.INTERNAL))
+          .build();
+    }
+  }
+
+  /**
    * Converts a byte array to a {@link BlobProto}.
    *
    * @param blobBytes the byte array to convert
diff --git a/java/tests/instrumentation/src/com/google/android/icing/IcingSearchEngineTest.java b/java/tests/instrumentation/src/com/google/android/icing/IcingSearchEngineTest.java
index 975699c..cf44b32 100644
--- a/java/tests/instrumentation/src/com/google/android/icing/IcingSearchEngineTest.java
+++ b/java/tests/instrumentation/src/com/google/android/icing/IcingSearchEngineTest.java
@@ -16,8 +16,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertThrows;
 
 import com.google.android.icing.IcingSearchEngine;
+import com.google.android.icing.proto.BatchGetResultProto;
 import com.google.android.icing.proto.BatchPutResultProto;
 import com.google.android.icing.proto.BlobProto;
 import com.google.android.icing.proto.DebugInfoResultProto;
@@ -28,13 +30,16 @@
 import com.google.android.icing.proto.DeleteResultProto;
 import com.google.android.icing.proto.DocumentProto;
 import com.google.android.icing.proto.GetAllNamespacesResultProto;
+import com.google.android.icing.proto.GetNextPageRequestProto;
 import com.google.android.icing.proto.GetOptimizeInfoResultProto;
 import com.google.android.icing.proto.GetResultProto;
 import com.google.android.icing.proto.GetResultSpecProto;
 import com.google.android.icing.proto.GetSchemaResultProto;
 import com.google.android.icing.proto.GetSchemaTypeResultProto;
+import com.google.android.icing.proto.HandleExpiredDocumentsResultProto;
 import com.google.android.icing.proto.IcingSearchEngineOptions;
 import com.google.android.icing.proto.InitializeResultProto;
+import com.google.android.icing.proto.JoinableConfig;
 import com.google.android.icing.proto.LogSeverity;
 import com.google.android.icing.proto.OptimizeResultProto;
 import com.google.android.icing.proto.PersistToDiskResultProto;
@@ -65,6 +70,7 @@
 import com.google.android.icing.proto.TermMatchType;
 import com.google.android.icing.proto.TermMatchType.Code;
 import com.google.android.icing.proto.UsageReport;
+import com.google.common.collect.ImmutableList;
 import com.google.android.icing.protobuf.ByteString;
 import java.io.File;
 import java.io.FileDescriptor;
@@ -212,9 +218,6 @@
     assertThat(getSchemaTypeResultProto.getSchemaTypeConfig()).isEqualTo(emailTypeConfig);
   }
 
-  // TODO: b/383379132 - Re-enable this test once the JNI API is pre-registered and dropped back
-  // into g3.
-  @Ignore
   @Test
   public void setAndGetSchemaWithDatabase_ok() throws Exception {
     IcingSearchEngineOptions options =
@@ -316,6 +319,7 @@
             .build();
     BatchPutResultProto batchPutResultProto = icingSearchEngine.batchPut(putDocumentRequest);
 
+    assertStatusOk(batchPutResultProto.getStatus());
     assertThat(batchPutResultProto.getPutResultProtos(0).getUri()).isEqualTo("uri1");
     assertStatusOk(batchPutResultProto.getPutResultProtos(0).getStatus());
     assertThat(batchPutResultProto.getPutResultProtos(1).getUri()).isEqualTo("uri2");
@@ -326,16 +330,81 @@
     assertThat(batchPutResultProto.getPersistToDiskResultProto().getStatus().getCode())
         .isEqualTo(StatusProto.Code.UNKNOWN);
 
-    // Check document 1
-    GetResultProto getResultProto =
-        icingSearchEngine.get("namespace", "uri1", GetResultSpecProto.getDefaultInstance());
-    assertStatusOk(getResultProto.getStatus());
-    assertThat(getResultProto.getDocument()).isEqualTo(emailDocument1);
-    // check document 2
-    getResultProto =
-        icingSearchEngine.get("namespace", "uri2", GetResultSpecProto.getDefaultInstance());
-    assertStatusOk(getResultProto.getStatus());
-    assertThat(getResultProto.getDocument()).isEqualTo(emailDocument2);
+    GetResultSpecProto getResultSpecProto =
+        GetResultSpecProto.newBuilder()
+            .setNamespaceRequested("namespace")
+            .addIds("uri1")
+            .addIds("uri2")
+            .build();
+    BatchGetResultProto batchGetResultProto = icingSearchEngine.batchGet(getResultSpecProto);
+
+    assertStatusOk(batchGetResultProto.getStatus());
+    // Check doc1
+    DocumentProto document = batchGetResultProto.getGetResultProtos(0).getDocument();
+    assertStatusOk(batchGetResultProto.getGetResultProtos(0).getStatus());
+    assertThat(document).isEqualTo(emailDocument1);
+    // Check doc2
+    document = batchGetResultProto.getGetResultProtos(1).getDocument();
+    assertStatusOk(batchGetResultProto.getGetResultProtos(1).getStatus());
+    assertThat(document).isEqualTo(emailDocument2);
+  }
+
+  @Test
+  public void testBatchGetWithEmptyResult() throws Exception {
+    assertStatusOk(icingSearchEngine.initialize().getStatus());
+
+    SchemaTypeConfigProto emailTypeConfig = createEmailTypeConfig();
+    SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build();
+    assertThat(
+            icingSearchEngine
+                .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
+                .getStatus()
+                .getCode())
+        .isEqualTo(StatusProto.Code.OK);
+
+    DocumentProto emailDocument1 = createEmailDocument("namespace", "uri1");
+    DocumentProto emailDocument2 = createEmailDocument("namespace", "uri2");
+    PutDocumentRequest putDocumentRequest =
+        PutDocumentRequest.newBuilder()
+            .addDocuments(emailDocument1)
+            .addDocuments(emailDocument2)
+            .build();
+    BatchPutResultProto batchPutResultProto = icingSearchEngine.batchPut(putDocumentRequest);
+    assertStatusOk(batchPutResultProto.getStatus());
+
+    // no ids.
+    GetResultSpecProto getResultSpecProto =
+        GetResultSpecProto.newBuilder().setNamespaceRequested("namespace").build();
+    BatchGetResultProto batchGetResultProto = icingSearchEngine.batchGet(getResultSpecProto);
+
+    // Check no doc returned if no ids are specified.
+    assertStatusOk(batchGetResultProto.getStatus());
+    assertThat(batchGetResultProto.getGetResultProtosList()).isEmpty();
+
+    // empty namespace.
+    getResultSpecProto = GetResultSpecProto.newBuilder().addIds("uri1").build();
+    batchGetResultProto = icingSearchEngine.batchGet(getResultSpecProto);
+    assertStatusOk(batchGetResultProto.getStatus());
+    assertThat(batchGetResultProto.getGetResultProtosList()).hasSize(1);
+    assertThat(batchGetResultProto.getGetResultProtos(0).getStatus().getCode())
+        .isEqualTo(StatusProto.Code.NOT_FOUND);
+
+    // different namespace.
+    getResultSpecProto =
+        GetResultSpecProto.newBuilder()
+            .setNamespaceRequested("otherNameSpace")
+            .addIds("uri1")
+            .addIds("uri2")
+            .build();
+    batchGetResultProto = icingSearchEngine.batchGet(getResultSpecProto);
+
+    // Check not found returned if namespace is different.
+    assertStatusOk(batchGetResultProto.getStatus());
+    assertThat(batchGetResultProto.getGetResultProtosList()).hasSize(2);
+    assertThat(batchGetResultProto.getGetResultProtos(0).getStatus().getCode())
+        .isEqualTo(StatusProto.Code.NOT_FOUND);
+    assertThat(batchGetResultProto.getGetResultProtos(1).getStatus().getCode())
+        .isEqualTo(StatusProto.Code.NOT_FOUND);
   }
 
   @Test
@@ -391,13 +460,18 @@
 
     PutDocumentRequest putDocumentRequest = PutDocumentRequest.getDefaultInstance();
     BatchPutResultProto batchPutResultProto = icingSearchEngine.batchPut(putDocumentRequest);
+    BatchPutResultProto actualProto =
+        batchPutResultProto.toBuilder().clearVmBinderTransactionLatencyStartTimeMs().build();
 
-    BatchPutResultProto expected = BatchPutResultProto.getDefaultInstance();
-    assertThat(batchPutResultProto).isEqualTo(expected);
+    BatchPutResultProto expected =
+        BatchPutResultProto.newBuilder()
+            .setStatus(StatusProto.newBuilder().setCode(StatusProto.Code.OK))
+            .build();
+    assertThat(actualProto).isEqualTo(expected);
 
     // PersistToDiskResultProto should not be set if persist_type is not set in the
     // PutDocumentRequest.
-    assertThat(batchPutResultProto.getPersistToDiskResultProto().getStatus().getCode())
+    assertThat(actualProto.getPersistToDiskResultProto().getStatus().getCode())
         .isEqualTo(StatusProto.Code.UNKNOWN);
   }
 
@@ -593,6 +667,84 @@
     assertThat(searchResultProto.getResultsCount()).isEqualTo(0);
   }
 
+  // TODO: b/417644758 - Re-enable this test once the JNI API is pre-registered and dropped back
+  // into g3.
+  @Ignore
+  @Test
+  public void getNextPageWithRequestProto() throws Exception {
+    assertStatusOk(icingSearchEngine.initialize().getStatus());
+
+    SchemaTypeConfigProto emailTypeConfig = createEmailTypeConfig();
+    SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build();
+    assertThat(
+            icingSearchEngine
+                .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
+                .getStatus()
+                .getCode())
+        .isEqualTo(StatusProto.Code.OK);
+
+    Map<String, DocumentProto> documents = new HashMap<>();
+    for (int i = 0; i < 10; i++) {
+      DocumentProto emailDocument =
+          createEmailDocument("namespace", "uri:" + i).toBuilder()
+              .addProperties(PropertyProto.newBuilder().setName("subject").addStringValues("foo"))
+              .build();
+      documents.put("uri:" + i, emailDocument);
+      assertWithMessage(icingSearchEngine.put(emailDocument).getStatus().getMessage())
+          .that(icingSearchEngine.put(emailDocument).getStatus().getCode())
+          .isEqualTo(StatusProto.Code.OK);
+    }
+
+    SearchSpecProto searchSpec =
+        SearchSpecProto.newBuilder()
+            .setQuery("foo")
+            .setTermMatchType(TermMatchType.Code.PREFIX)
+            .build();
+    ResultSpecProto resultSpecProto = ResultSpecProto.newBuilder().setNumPerPage(2).build();
+
+    SearchResultProto searchResultProto =
+        icingSearchEngine.search(
+            searchSpec, ScoringSpecProto.getDefaultInstance(), resultSpecProto);
+    assertStatusOk(searchResultProto.getStatus());
+    assertThat(searchResultProto.getResultsCount()).isEqualTo(2);
+    DocumentProto resultDocument1 = searchResultProto.getResults(0).getDocument();
+    DocumentProto resultDocument2 = searchResultProto.getResults(1).getDocument();
+    assertThat(resultDocument1).isEqualTo(documents.remove(resultDocument1.getUri()));
+    assertThat(resultDocument2).isEqualTo(documents.remove(resultDocument2.getUri()));
+
+    assertThat(searchResultProto.getQueryStats().hasNativeToJavaStartTimestampMs()).isTrue();
+    assertThat(searchResultProto.getQueryStats().hasNativeToJavaJniLatencyMs()).isTrue();
+    assertThat(searchResultProto.getQueryStats().hasJavaToNativeJniLatencyMs()).isTrue();
+    assertThat(searchResultProto.getQueryStats().getNativeToJavaStartTimestampMs())
+        .isGreaterThan(0);
+    assertThat(searchResultProto.getQueryStats().getNativeToJavaJniLatencyMs()).isAtLeast(0);
+    assertThat(searchResultProto.getQueryStats().getJavaToNativeJniLatencyMs()).isAtLeast(0);
+
+    GetNextPageRequestProto getNextPageRequestProto =
+        GetNextPageRequestProto.newBuilder()
+            .setNextPageToken(searchResultProto.getNextPageToken())
+            .setMaxResultsToRetrieveFromPage(1)
+            .build();
+    // fetch rest pages
+    for (int i = 1; i < 5; i++) {
+      searchResultProto = icingSearchEngine.getNextPage(getNextPageRequestProto);
+      DocumentProto resultDocument = searchResultProto.getResults(0).getDocument();
+      assertWithMessage(searchResultProto.getStatus().getMessage())
+          .that(searchResultProto.getStatus().getCode())
+          .isEqualTo(StatusProto.Code.OK);
+      assertThat(searchResultProto.getResultsCount()).isEqualTo(1);
+      assertThat(resultDocument).isEqualTo(documents.remove(resultDocument.getUri()));
+    }
+
+    // invalidate rest result
+    icingSearchEngine.invalidateNextPageToken(searchResultProto.getNextPageToken());
+
+    searchResultProto = icingSearchEngine.getNextPage(getNextPageRequestProto);
+    assertStatusOk(searchResultProto.getStatus());
+    assertThat(searchResultProto.getResultsCount()).isEqualTo(0);
+  }
+
+
   @Ignore // b/350530146
   @Test
   public void writeAndReadBlob_blobContentMatches() throws Exception {
@@ -606,7 +758,7 @@
     IcingSearchEngine icing = new IcingSearchEngine(options);
     assertStatusOk(icing.initialize().getStatus());
 
-    byte[] data = generateRandomBytes(100); // 10 Bytes
+    byte[] data = generateRandomBytes(100); // 100 Bytes
     byte[] digest = calculateDigest(data);
     PropertyProto.BlobHandleProto blobHandle =
         PropertyProto.BlobHandleProto.newBuilder()
@@ -661,7 +813,7 @@
     IcingSearchEngine icing = new IcingSearchEngine(options);
     assertStatusOk(icing.initialize().getStatus());
 
-    byte[] data = generateRandomBytes(100); // 10 Bytes
+    byte[] data = generateRandomBytes(100); // 100 Bytes
     byte[] digest = calculateDigest(data);
     PropertyProto.BlobHandleProto blobHandle =
         PropertyProto.BlobHandleProto.newBuilder()
@@ -694,6 +846,74 @@
     assertThat(commitBlobProto.getStatus().getCode()).isEqualTo(StatusProto.Code.NOT_FOUND);
   }
 
+  @Ignore // b/350530146
+  @Test
+  public void getAndPutBlobInfo() throws Exception {
+    // 1 Arrange: set up IcingSearchEngine with and blob data
+    File tempDir = temporaryFolder.newFolder();
+    IcingSearchEngineOptions options =
+        IcingSearchEngineOptions.newBuilder()
+            .setBaseDir(tempDir.getCanonicalPath())
+            .setEnableBlobStore(true)
+            .build();
+    IcingSearchEngine icing = new IcingSearchEngine(options);
+    assertStatusOk(icing.initialize().getStatus());
+
+    byte[] data = generateRandomBytes(100); // 100 Bytes
+    byte[] digest = calculateDigest(data);
+    PropertyProto.BlobHandleProto blobHandle =
+        PropertyProto.BlobHandleProto.newBuilder()
+            .setNamespace("ns")
+            .setDigest(ByteString.copyFrom(digest))
+            .build();
+
+    // 2 Act: write the blob
+    BlobProto openWriteBlobProto = icing.openWriteBlob(blobHandle);
+    assertStatusOk(openWriteBlobProto.getStatus());
+    Field field = FileDescriptor.class.getDeclaredField("fd");
+    field.setAccessible(true); // Make the field accessible
+
+    // Create a new FileDescriptor object
+    FileDescriptor writeFd = new FileDescriptor();
+
+    // Set the file descriptor value using reflection
+    field.setInt(writeFd, openWriteBlobProto.getFileDescriptor());
+
+    try (FileOutputStream outputStream = new FileOutputStream(writeFd)) {
+      outputStream.write(data);
+    }
+
+    // Commit and read the blob.
+    BlobProto commitBlobProto = icing.commitBlob(blobHandle);
+    assertStatusOk(commitBlobProto.getStatus());
+
+    // Get the blob info.
+    BlobProto allBlobInfoProto = icing.getAllBlobInfos();
+    assertStatusOk(allBlobInfoProto.getStatus());
+    assertThat(allBlobInfoProto.getBlobInfoProtosCount()).isEqualTo(1);
+
+    // create a new IcingSearchEngine instance.
+    IcingSearchEngineOptions options2 =
+        IcingSearchEngineOptions.newBuilder()
+            .setBaseDir(tempDir.getCanonicalPath())
+            .setEnableBlobStore(true)
+            .setManageBlobFiles(false)
+            .build();
+    IcingSearchEngine icing2 = new IcingSearchEngine(options2);
+    assertStatusOk(icing2.initialize().getStatus());
+
+    // Put the blob info.
+    BlobProto putBlobInfosProto = icing2.putBlobInfos(allBlobInfoProto);
+    assertStatusOk(putBlobInfosProto.getStatus());
+
+    // 3: Verify the blob info.
+    BlobProto allBlobInfoProto2 = icing2.getAllBlobInfos();
+    assertStatusOk(allBlobInfoProto2.getStatus());
+    assertThat(allBlobInfoProto2.getBlobInfoProtosCount()).isEqualTo(1);
+    assertThat(allBlobInfoProto2.getBlobInfoProtos(0))
+        .isEqualTo(allBlobInfoProto2.getBlobInfoProtos(0));
+  }
+
   @Test
   public void testDelete() throws Exception {
     assertStatusOk(icingSearchEngine.initialize().getStatus());
@@ -968,6 +1188,45 @@
   }
 
   @Test
+  public void testClearAndDestroy() throws Exception {
+    assertStatusOk(icingSearchEngine.initialize().getStatus());
+
+    // Simple put and get
+    SchemaTypeConfigProto emailTypeConfig = createEmailTypeConfig();
+    SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build();
+    assertThat(
+            icingSearchEngine
+                .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
+                .getStatus()
+                .getCode())
+        .isEqualTo(StatusProto.Code.OK);
+
+    DocumentProto emailDocument = createEmailDocument("namespace", "uri");
+    PutResultProto putResultProto = icingSearchEngine.put(emailDocument);
+    assertStatusOk(putResultProto.getStatus());
+
+    GetResultProto getResultProto =
+        icingSearchEngine.get("namespace", "uri", GetResultSpecProto.getDefaultInstance());
+    assertStatusOk(getResultProto.getStatus());
+    assertThat(getResultProto.getDocument()).isEqualTo(emailDocument);
+
+    // Clear and destroy
+    ResetResultProto clearAndDestroyResult = icingSearchEngine.clearAndDestroy();
+    assertStatusOk(clearAndDestroyResult.getStatus());
+
+    // Try to put and get again, but it should fail since the instance is
+    // uninitialized after clearAndDestroy().
+    assertThat(icingSearchEngine.put(emailDocument).getStatus().getCode())
+        .isEqualTo(StatusProto.Code.FAILED_PRECONDITION);
+    assertThat(
+            icingSearchEngine
+                .get("namespace", "uri", GetResultSpecProto.getDefaultInstance())
+                .getStatus()
+                .getCode())
+        .isEqualTo(StatusProto.Code.FAILED_PRECONDITION);
+  }
+
+  @Test
   public void testReportUsage() throws Exception {
     assertStatusOk(icingSearchEngine.initialize().getStatus());
 
@@ -1205,4 +1464,44 @@
   private static void assertStatusOk(StatusProto status) {
     assertWithMessage(status.getMessage()).that(status.getCode()).isEqualTo(StatusProto.Code.OK);
   }
+
+  @Test
+  public void throwIfClosed() throws Exception {
+    icingSearchEngine.close();
+    assertThrows(IllegalStateException.class, () -> icingSearchEngine.initialize());
+    assertThrows(
+        IllegalStateException.class,
+        () -> icingSearchEngine.setSchema(SchemaProto.getDefaultInstance()));
+    assertThrows(IllegalStateException.class, () -> icingSearchEngine.getSchema());
+    assertThrows(IllegalStateException.class, () -> icingSearchEngine.getSchemaType("type"));
+    assertThrows(
+        IllegalStateException.class,
+        () -> icingSearchEngine.put(DocumentProto.getDefaultInstance()));
+    assertThrows(
+        IllegalStateException.class,
+        () -> icingSearchEngine.batchPut(PutDocumentRequest.getDefaultInstance()));
+    assertThrows(
+        IllegalStateException.class,
+        () -> icingSearchEngine.get("namespace", "uri", GetResultSpecProto.getDefaultInstance()));
+    assertThrows(
+        IllegalStateException.class,
+        () -> icingSearchEngine.batchGet(GetResultSpecProto.getDefaultInstance()));
+    assertThrows(IllegalStateException.class, () -> icingSearchEngine.optimize());
+    assertThrows(
+        IllegalStateException.class, () -> icingSearchEngine.persistToDisk(PersistType.Code.LITE));
+    assertThrows(
+        IllegalStateException.class,
+        () -> icingSearchEngine.reportUsage(UsageReport.getDefaultInstance()));
+    assertThrows(IllegalStateException.class, () -> icingSearchEngine.getStorageInfo());
+    assertThrows(
+        IllegalStateException.class,
+        () ->
+            icingSearchEngine.search(
+                SearchSpecProto.getDefaultInstance(),
+                ScoringSpecProto.getDefaultInstance(),
+                ResultSpecProto.getDefaultInstance()));
+    assertThrows(
+        IllegalStateException.class,
+        () -> icingSearchEngine.getNextPage(GetNextPageRequestProto.getDefaultInstance()));
+  }
 }
diff --git a/proto/appsearch/account.proto b/proto/appsearch/account.proto
new file mode 100644
index 0000000..0ee8afa
--- /dev/null
+++ b/proto/appsearch/account.proto
@@ -0,0 +1,36 @@
+
+// Copyright (C) 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+syntax = "proto2";
+
+package appsearch;
+
+option java_package = "com.google.android.appsearch.proto";
+option java_multiple_files = true;
+option objc_class_prefix = "APPSEARCH";
+
+// The proto to reprent an AppSearchAccount object
+// Next tag: 6
+message AccountProto {
+  // The namespace the account document belongs to .
+  optional string namespace = 1;
+  // The unique identifier for the document within the namespace.
+  optional string id = 2;
+  // The type of the account. This corresponds to android.accounts.Account.type.
+  optional string account_type = 3;
+  // The name of the account. This corresponds to android.accounts.Account.name.
+  optional string account_name = 4;
+  // An opaque, optional identifier for the account, like gaia id.
+  optional string account_id = 5;
+}
diff --git a/proto/icing/proto/blob.proto b/proto/icing/proto/blob.proto
index b9a8fc2..0df640a 100644
--- a/proto/icing/proto/blob.proto
+++ b/proto/icing/proto/blob.proto
@@ -26,7 +26,7 @@
 // Defines the blob operation result proto that user try to
 // read/write/commit/remove a blob to Icing.
 //
-// Next tag: 4
+// Next tag: 7
 message BlobProto {
   // Status code can be one of:
   //   OK
@@ -43,6 +43,18 @@
   // If Icing does not manage blob files, this is the file name of the blob file
   // from Icing returned by OpenWriteBlob, OpenReadBlob and RemoveBlob.
   optional string file_name = 3;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 4;
+
+  // The blob info protos that are stored in the blob info proto log file. This
+  // is used migrate the blob info proto log file to another Icing instance.
+  // This field will only be set via GetAllBlobInfo call,
+  repeated BlobInfoProto blob_info_protos = 5;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 6;
 }
 
 // BlobInfo holds information about a blob. It is used to store in the blob
diff --git a/proto/icing/proto/document.proto b/proto/icing/proto/document.proto
index da82d60..84870d6 100644
--- a/proto/icing/proto/document.proto
+++ b/proto/icing/proto/document.proto
@@ -130,17 +130,30 @@
 }
 
 // Holds a list of PutResultProto.
-// Next tag: 3
+// Next tag: 6
 message BatchPutResultProto {
+  // The overall status of doing the batch put. It is independent of the status
+  // of the individual PutResultProtos. E.g. we may still return OK here even if
+  // all the PutResultProto have errors. Or return INTERNAL_ERROR if there is
+  // some issue parsing this proto, even if all documents are indexed
+  // successfully.
+  optional StatusProto status = 3;
   repeated PutResultProto put_result_protos = 1;
 
   // The result of calling PersistToDisk at the end of the BatchPut request if
   // PutDocumentRequest.persist_type is set.
   optional PersistToDiskResultProto persist_to_disk_result_proto = 2;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 4;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 5;
 }
 
 // Result of a call to IcingSearchEngine.Put
-// Next tag: 5
+// Next tag: 8
 message PutResultProto {
   // Status code can be one of:
   //   OK
@@ -166,10 +179,26 @@
 
   // Whether or not the document was a replacement of an existing document.
   optional bool was_replacement = 3;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 5;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 6;
+
+  // The expiration timestamp (in milliseconds) of the newly added document.
+  // This field can be used by the caller (mostly AppSearch) for expired
+  // document and expiration propagation handling.
+  // - INT64_MAX means the new document never expires.
+  // - This field is set only when the document is added successfully (i.e.
+  //   status is OK). The caller should ignore this field if the value is 0,
+  //   since it is impossible to have a document expires at t = 0.
+  optional int64 document_expiration_timestamp_ms = 7;
 }
 
 // Result of a call to IcingSearchEngine.Get
-// Next tag: 3
+// Next tag: 6
 message GetResultProto {
   // Status code can be one of:
   //   OK
@@ -183,13 +212,41 @@
   // go/icing-library-apis.
   optional StatusProto status = 1;
 
+  // The uri of the document that was retrieved, or failed to be retrieved.
+  // It won't be set now for a single Get call, as the caller knows the uri if
+  // the Get fails.
+  // TODO(b/404275015) We should always set this field in Get once we refactor
+  // the tests.
+  optional string uri = 3;
+
   // Copy of the Document proto with the specified name_space, uri. Modifying
   // this does not affect the Document in IcingSearchEngine.
   optional DocumentProto document = 2;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 4;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 5;
+}
+
+// Result of a call to IcingSearchEngine.BatchGet
+// Next tag: 5
+message BatchGetResultProto {
+  optional StatusProto status = 1;
+  repeated GetResultProto get_result_protos = 2;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 3;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 4;
 }
 
 // Result of a call to IcingSearchEngine.GetAllNamespaces
-// Next tag: 3
+// Next tag: 5
 message GetAllNamespacesResultProto {
   // Status code can be one of:
   //   OK
@@ -200,10 +257,17 @@
   // List of namespaces which have at least one existing document in it (not
   // deleted and not expired). Order of namespaces is undefined.
   repeated string namespaces = 2;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 3;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 4;
 }
 
 // Result of a call to IcingSearchEngine.Delete
-// Next tag: 3
+// Next tag: 5
 message DeleteResultProto {
   // Status code can be one of:
   //   OK
@@ -219,10 +283,17 @@
 
   // Stats for delete execution performance.
   optional DeleteStatsProto delete_stats = 2;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 3;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 4;
 }
 
 // Result of a call to IcingSearchEngine.DeleteByNamespace
-// Next tag: 3
+// Next tag: 5
 message DeleteByNamespaceResultProto {
   // Status code can be one of:
   //   OK
@@ -238,10 +309,17 @@
 
   // Stats for delete execution performance.
   optional DeleteStatsProto delete_stats = 2;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 3;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 4;
 }
 
 // Result of a call to IcingSearchEngine.DeleteBySchemaType
-// Next tag: 3
+// Next tag: 5
 message DeleteBySchemaTypeResultProto {
   // Status code can be one of:
   //   OK
@@ -257,10 +335,17 @@
 
   // Stats for delete execution performance.
   optional DeleteStatsProto delete_stats = 2;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 3;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 4;
 }
 
 // Result of a call to IcingSearchEngine.DeleteByQuery
-// Next tag: 5
+// Next tag: 7
 message DeleteByQueryResultProto {
   // Status code can be one of:
   //   OK
@@ -291,4 +376,49 @@
   repeated DocumentGroupInfo deleted_documents = 4;
 
   reserved 2;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 5;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 6;
+}
+
+// Next tag: 6
+message HandleExpiredDocumentsResultProto {
+  // Status code can be one of:
+  //   OK
+  //   FAILED_PRECONDITION
+  //   NOT_FOUND
+  //   INTERNAL
+  //
+  // See status.proto for more details.
+  //
+  // TODO(b/147699081): Fix error codes: +ABORTED.
+  // go/icing-library-apis.
+  optional StatusProto status = 1;
+
+  // Metadata for the deleted documents, grouped by namespace and schema.
+  message DocumentGroupInfo {
+    optional string name_space = 1;
+    optional string schema = 2;
+    repeated string uris = 3;
+  }
+  // A list of groups of deleted documents. A document was deleted because:
+  // - It was expired.
+  // - OR it was a child document of an expired document with delete propagation
+  //   enabled.
+  repeated DocumentGroupInfo deleted_documents = 2;
+
+  // The number of expired documents that were deleted. This DOES NOT include
+  // the propagated child documents.
+  optional int32 num_expired_documents = 3;
+
+  // The number of deleted documents that were propagated from the expired
+  // documents.
+  optional int32 num_propagated_deleted_documents = 4;
+
+  // The expiration timestamp (in milliseconds) of the next expired document.
+  optional int64 next_expiration_timestamp_ms = 5;
 }
diff --git a/proto/icing/proto/initialize.proto b/proto/icing/proto/initialize.proto
index 5977aa9..40a2af0 100644
--- a/proto/icing/proto/initialize.proto
+++ b/proto/icing/proto/initialize.proto
@@ -17,6 +17,7 @@
 package icing.lib;
 
 import "icing/proto/logging.proto";
+import "icing/proto/persist.proto";
 import "icing/proto/status.proto";
 
 option java_package = "com.google.android.icing.proto";
@@ -71,8 +72,18 @@
     // join index v3 will be rebuilt to replace v2.
     FEATURE_QUALIFIED_ID_JOIN_INDEX_V3 = 6;
 
-    // TODO(b/384947619): decide whether need to add a feature type for delete
-    // propagation.
+    // Feature for flag
+    // IcingSearchEngineOptions::enable_delete_propagation_from.
+    //
+    // This feature covers whether to enable delete propagation PROPAGATE_FROM.
+    FEATURE_DELETE_PROPAGATION_FROM = 7;
+
+    // Feature for flag
+    // IcingSearchEngineOptions::enable_non_existent_qualified_id_join.
+    //
+    // This feature enables handling of cases where a child document is indexed
+    // before its join parent document.
+    FEATURE_NON_EXISTENT_QUALIFIED_ID_JOIN = 8;
   }
 
   // Whether the feature requires the document store to be rebuilt.
@@ -137,7 +148,7 @@
   optional OperationType.Code operation_type = 1;
 }
 
-// Next tag: 35
+// Next tag: 54
 message IcingSearchEngineOptions {
   // Directory to persist files for Icing. Required.
   // If Icing was previously initialized with this directory, it will reload
@@ -193,6 +204,16 @@
   // Optional.
   optional int32 compression_level = 7 [default = 3];
 
+  // Memory level for compression.
+  // 1 uses minimum memory but is slow and reduces compression ratio; 9 uses
+  // maximum memory for optimal speed and compression ratio.
+  // Icing historically used a memLevel of 8. Therefore, that value will be the
+  // default.
+  //
+  // Valid values: [1, 9]
+  // Optional.
+  optional int32 compression_mem_level = 37 [default = 8];
+
   // OPTIONAL: Whether to allow circular references between schema types for
   // the schema definition.
   //
@@ -290,21 +311,19 @@
   // Optional.
   optional int32 blob_store_compression_level = 23 [default = 3];
 
+  // Memory level for compression in blob store.
+  // 1 uses minimum memory but is slow and reduces compression ratio; 9 uses
+  // maximum memory for optimal speed and compression ratio.
+  // Icing historically used a memLevel of 8. Therefore, that value will be the
+  // default.
+  //
+  // Valid values: [1, 9]
+  // Optional.
+  optional int32 blob_store_compression_mem_level = 38 [default = 8];
+
   // Whether to allow repeated fields to have a joinable value type.
   optional bool enable_repeated_field_joins = 24;
 
-  // DEPRECATED (separate them into 2 flags): whether to use qualified id join
-  // index v3 and enable delete propagation PROPAGATE_FROM.
-  //
-  // - If set to true, qualified id join index v3 will be created and delete
-  //   propagation PROPAGATE_FROM will be enabled.
-  // - Otherwise, qualified id join index v2 will be created and delete
-  //   propagation will be disabled.
-  //
-  // The default value is false.
-  optional bool enable_qualified_id_join_index_v3_and_delete_propagate_from =
-      25;
-
   // The absolute path to the ICU data file.
   // If set, ICU will be initialized using this data file.
   optional string icu_data_file_absolute_path = 26;
@@ -361,11 +380,121 @@
   // the overlay exists.
   optional bool release_backup_schema_file_if_overlay_present = 34;
 
-  reserved 2, 20;
+  // Whether to enable strict byte size enforcement on a result page.
+  // - If set to true, then ResultRetrieverV2 will not include the next result
+  //   document if it would exceed the page byte size threshold (except for the
+  //   first document).
+  // - Otherwise, the page byte size enforcement works as a soft limit, i.e. it
+  //   will keep adding documents until exceeding the threshold.
+  optional bool enable_strict_page_byte_size_limit = 35;
+
+  // The threshold in bytes for compressing documents. If a document is larger
+  // than or equal to this threshold, it will be compressed based on
+  // compression_level. 0 means always compress.
+  optional uint32 compression_threshold_bytes = 36;
+
+  // Whether to enable using smaller input buffer size for decompression
+  // If set to false, the default buffer size of 64kb will be used.
+  optional bool enable_smaller_decompression_buffer_size = 39;
+
+  // Whether to enable the Eigen library for embedding scoring.
+  // If set to true **and** Eigen is compiled in (when ICING_DISABLE_EIGEN is
+  // not defined), Eigen will be used for embedding scoring.
+  optional bool enable_eigen_embedding_scoring = 40;
+
+  // Whether to enable passing down filter predicates to child iterators during
+  // DocHitInfo retrieval. If set to true, filter predicates may be passed to
+  // children for optimized retrieval.
+  optional bool enable_passing_filter_to_children = 41;
+
+  // Whether to enable the new header format (refactor legacy format and
+  // introduce unsynced tail checksum) related changes in
+  // PortableFileBackedProtoLog.
+  optional bool enable_proto_log_new_header_format = 42;
+
+  // Whether to enable the new embedding iterator DocHitInfoIteratorEmbeddingV2.
+  // This new iterator optimizes the access order of the embedding data for
+  // sequential reads.
+  optional bool enable_embedding_iterator_v2 = 43;
+
+  // Whether PortableFileBackedProtoLog should retain a decompression buffer
+  // that reads can reuse rather than allocating a new one for each read.
+  optional bool enable_reusable_decompression_buffer = 44;
+
+  // Whether to enable schema-type id optimization for setSchema. When enabled,
+  // the type-ids of existing types will be preserved when possible, and there
+  // will be no schema type-id reassignment for adding new types.
+  optional bool enable_schema_type_id_optimization = 45;
+
+  // The number of shards to use for the embedding index. 1 means no sharding.
+  optional uint32 embedding_index_num_shards = 46 [default = 1];
+
+  // Whether to enable optimize improvements.
+  optional bool enable_optimize_improvements = 47;
+
+  // The time threshold for an expired document to be purged.
+  // - Since we schedule a task to purge expired documents according to the next
+  //   expiration time of the documents, it is possible that some documents
+  //   expire within a small time window and the task executes too frequently.
+  // - Therefore, we use this flag to purge more documents that also expire in a
+  //   short period of time after the current time.
+  //
+  // For example, if the value is 1000 ms and the current time is 10000 ms:
+  // - All documents that are expired before 10000 ms will be purged, since they
+  //   are already expired.
+  // - Additionally, we will also purge documents that expire in the next 1000
+  //   ms, i.e. (10000, 11000] ms.
+  //
+  // The default value is 1 minute (1 * 60 * 1000 ms).
+  optional int64 expired_document_purge_threshold_ms = 48 [default = 60000];
+
+  // Whether to allow a document to reference a join parent (by its qualified
+  // id) that does not yet exist. When enabled, the join index will handle cases
+  // where a child document is indexed before its parent. The join relationship
+  // will be established once the parent document is indexed. If disabled, the
+  // join relationship will be lost if the child is indexed before the parent.
+  optional bool enable_non_existent_qualified_id_join = 49;
+
+  // Whether to skip the schema type equality check during SetSchema. We
+  // serialize the schema proto to strings when doing this check, which is slow.
+  //
+  // This will be set to true in AppSearch.
+  // - AppSearch already checks if schema types are unchanged for a new
+  //   SetSchema request, and skips the Icing interaction entirely if that is
+  //   the case.
+  // - Therefore, the Icing-side equality check in SetSchema is redundant and
+  //   can be skipped if caller is AppSearch.
+  optional bool enable_skip_set_schema_type_equality_check = 50;
+
+  // Whether to enable the background task scheduler. This value should be true
+  // for all Icing lib direct clients (e.g. iOS), and false for AppSearch since
+  // task scheduling must be handled at the AppSearch layer.
+  //
+  // If set to true, the background task scheduler will be enabled. Otherwise,
+  // the background task scheduler will be disabled.
+  optional bool enable_background_task_scheduler =
+      51;  // TODO(b/384947619): set default to true
+
+  // Whether to enable a query optimization that will rewrite embedding query
+  // iterators that are being AND'ed with other iterators such that those other
+  // iterators can be pushed down into the embedding iterator as a delegate.
+  // This allows us to avoid reading and scoring embeddings for documents that
+  // don't match the other requirements of the query.
+  optional bool enable_embed_query_optimization = 52;
+
+  // Whether to enable deduping schema types's property definitions when
+  // storing SchemaTypeConfigs in the schema store.
+  //
+  // Once enabled, type configs with the same property definitions will be
+  // deduped, and only one of the types will be stored with full property
+  // definitions.
+  optional bool enable_schema_definition_deduping = 53;
+
+  reserved 2, 20, 25;
 }
 
 // Result of a call to IcingSearchEngine.Initialize
-// Next tag: 3
+// Next tag: 6
 message InitializeResultProto {
   // Status code can be one of:
   //   OK
@@ -386,6 +515,17 @@
   // logging.proto for details.
   optional InitializeStatsProto initialize_stats = 2;
 
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 3;
+
+  // Persist type needed after initialization. If UNKNOWN, then everything is
+  // clean and no need to call persistToDisk.
+  optional PersistType.Code needs_persist_type = 4;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 5;
+
   // TODO(b/147699081): Add a field to indicate lost_schema and lost_documents.
   // go/icing-library-apis.
 }
diff --git a/proto/icing/proto/logging.proto b/proto/icing/proto/logging.proto
index 2e620cd..783111d 100644
--- a/proto/icing/proto/logging.proto
+++ b/proto/icing/proto/logging.proto
@@ -24,7 +24,7 @@
 option objc_class_prefix = "ICNG";
 
 // Stats of the top-level function IcingSearchEngine::Initialize().
-// Next tag: 17
+// Next tag: 21
 message InitializeStatsProto {
   // Overall time used for the function call.
   optional int32 latency_ms = 1;
@@ -150,6 +150,79 @@
 
   // Number of documents that failed to be reindexed during index restoration.
   optional int32 num_failed_reindexed_documents = 16;
+
+  // A number indicating which stage of initialization failed.
+  message FailureStage {
+    enum Code {
+      NONE = 0;
+
+      OPTIONS_VALIDATION = 1;
+
+      BASE_DIRECTORY_CREATION = 2;
+
+      INIT_MARKER_FILE = 3;
+
+      READ_VERSION_FILE = 4;
+
+      MIGRATE_SCHEMA = 5;
+
+      DISCARD_DERIVED_FILES = 6;
+
+      WRITE_VERSION_FILE = 7;
+
+      SCHEMA_STORE_DIRECTORY_CREATION = 8;
+
+      SCHEMA_STORE_INSTANTIATION = 9;
+
+      LANGUAGE_SEGMENTER_CREATION = 10;
+
+      NORMALIZER_CREATION = 11;
+
+      // Discard any components, including delete the directory or call discard
+      // function of an index.
+      DISCARD_COMPONENTS = 12;
+
+      BLOB_STORE_INSTANTIATION = 13;
+
+      DOCUMENT_STORE_DIRECTORY_CREATION = 14;
+
+      DOCUMENT_STORE_INSTANTIATION = 15;
+
+      TERM_INDEX_DIRECTORY = 16;
+
+      TERM_INDEX_INSTANTIATION = 17;
+
+      INTEGER_INDEX_DIRECTORY = 18;
+
+      INTEGER_INDEX_INSTANTIATION = 19;
+
+      QUALIFIED_ID_JOIN_INDEX_DIRECTORY = 20;
+
+      QUALIFIED_ID_JOIN_INDEX_INSTANTIATION = 21;
+
+      EMBEDDING_INDEX_DIRECTORY = 22;
+
+      EMBEDDING_INDEX_INSTANTIATION = 23;
+
+      RESTORE_INDEX = 24;
+    }
+  }
+  optional FailureStage.Code failure_stage = 17;
+
+  // Status of the attempt to create the IcuLanguageSegmenter.
+  // This is only set if the ICU segmenter is enabled. Possible status codes:
+  // - OK: IcuLanguageSegmenter created successfully.
+  // - ABORTED: Failed to create the ICU break iterator.
+  optional StatusProto icu_segmenter_creation_status = 18;
+
+  // Status of the attempt to create the IcuNormalizer.
+  // This is only set if the ICU normalizer is enabled. Possible status codes:
+  // - OK: IcuNormalizer created successfully.
+  // - INTERNAL: Failed to create the ICU Transliterator.
+  optional StatusProto icu_normalizer_creation_status = 19;
+
+  // The expiration timestamp (in milliseconds) of the next expired document.
+  optional int64 next_expiration_timestamp_ms = 20;
 }
 
 // Stats of the top-level function IcingSearchEngine::Put().
@@ -206,7 +279,7 @@
 
 // Stats of the top-level function IcingSearchEngine::Search() and
 // IcingSearchEngine::GetNextPage().
-// Next tag: 28
+// Next tag: 30
 message QueryStatsProto {
   // TODO(b/305098009): deprecate. Use parent_search_stats instead.
   // The UTF-8 length of the query string
@@ -288,7 +361,7 @@
   optional bool is_join_query = 23;
 
   // Stats of the search. Only valid for first page.
-  // Next tag: 16
+  // Next tag: 20
   message SearchStats {
     // The UTF-8 length of the query string
     optional int32 query_length = 1;
@@ -335,6 +408,18 @@
 
     // Time used in QueryVisitor to visit and build (nested) DocHitInfoIterator.
     optional int32 query_processor_query_visitor_latency_ms = 15;
+
+    // Number of unquantized embeddings scored.
+    optional int32 num_unquantized_embeddings_scored = 16;
+
+    // Number of quantized embeddings scored.
+    optional int32 num_quantized_embeddings_scored = 17;
+
+    // Number of shards read from the embedding index.
+    optional int32 num_embedding_shards_read = 18;
+
+    // Number of raw embedding bytes read from the embedding index.
+    optional int64 num_embedding_bytes_read = 19;
   }
 
   // Search stats for parent. Only valid for first page.
@@ -349,6 +434,28 @@
   // Byte size of the unsorted tail of the lite index hit buffer.
   optional int64 lite_index_hit_buffer_unsorted_byte_size = 27;
 
+  message PageTokenType {
+    enum Code {
+      // Default. Usually used when it is the first page.
+      NONE = 0;
+
+      VALID = 1;
+
+      // The current page token is not found in ResultStateManager. This is
+      // usually caused by cache eviction.
+      NOT_FOUND = 2;
+
+      // The current page token is empty (kInvalidNextPageToken).
+      EMPTY = 3;
+    }
+  }
+  // The type of the input page token.
+  optional PageTokenType.Code page_token_type = 28;
+
+  // Number of result states being force-evicted from ResultStateManager due to
+  // budget limit. This doesn't include expired or invalidated states.
+  optional int32 num_result_states_evicted = 29;
+
   reserved 9;
 }
 
diff --git a/proto/icing/proto/optimize.proto b/proto/icing/proto/optimize.proto
index 0ba0a86..12c65dc 100644
--- a/proto/icing/proto/optimize.proto
+++ b/proto/icing/proto/optimize.proto
@@ -16,6 +16,7 @@
 
 package icing.lib;
 
+import "icing/proto/persist.proto";
 import "icing/proto/status.proto";
 
 option java_package = "com.google.android.icing.proto";
@@ -23,7 +24,7 @@
 option objc_class_prefix = "ICNG";
 
 // Result of a call to IcingSearchEngine.Optimize
-// Next tag: 4
+// Next tag: 6
 message OptimizeResultProto {
   // Status code can be one of:
   //   OK
@@ -43,10 +44,17 @@
 
   // TODO(b/147699081): Add a field to indicate lost_schema and lost_documents.
   // go/icing-library-apis.
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 4;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 5;
 }
 
 // Result of a call to IcingSearchEngine.GetOptimizeInfo
-// Next tag: 5
+// Next tag: 9
 message GetOptimizeInfoResultProto {
   // Status code can be one of:
   //   OK
@@ -66,9 +74,28 @@
 
   // The amount of time since the last optimize ran.
   optional int64 time_since_last_optimize_ms = 4;
+
+  // True if there is no previous optimize info written to disk. This would
+  // indicate that Icing has never run optimize before.
+  optional bool no_previous_optimize_info = 5;
+
+  // The number of active result states in ResultStateManager.
+  //
+  // If this number is not 0, then the caller should avoid running optimize as
+  // much as possible to avoid result state manager reset and interrupted
+  // pagination, unless the last optimize run was a long time ago which exceeds
+  // the threshold.
+  optional int32 num_active_result_states = 6;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 7;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 8;
 }
 
-// Next tag: 14
+// Next tag: 16
 message OptimizeStatsProto {
   // Overall time used for the function call.
   optional int32 latency_ms = 1;
@@ -116,4 +143,10 @@
 
   // Number of namespaces deleted.
   optional int32 num_deleted_namespaces = 12;
+
+  // Stats of the PersistToDisk call before optimization.
+  optional PersistToDiskStatsProto before_optimize_persist_stats = 14;
+
+  // Stats of the PersistToDisk call after optimization.
+  optional PersistToDiskStatsProto after_optimize_persist_stats = 15;
 }
diff --git a/proto/icing/proto/persist.proto b/proto/icing/proto/persist.proto
index 802195d..7af2e98 100644
--- a/proto/icing/proto/persist.proto
+++ b/proto/icing/proto/persist.proto
@@ -57,7 +57,7 @@
 }
 
 // Result of a call to IcingSearchEngine.Persist
-// Next tag: 2
+// Next tag: 5
 message PersistToDiskResultProto {
   // Status code can be one of:
   //   OK
@@ -66,4 +66,71 @@
   //
   // See status.proto for more details.
   optional StatusProto status = 1;
+
+  // Stats about the PersistToDisk call.
+  optional PersistToDiskStatsProto persist_stats = 2;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 3;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 4;
+}
+
+// Stats of the top-level function IcingSearchEngine::PersistToDisk.
+// Next tag: 14
+message PersistToDiskStatsProto {
+  // The type of persistence guarantee that PersistToDisk should provide.
+  optional PersistType.Code persist_type = 1;
+
+  // Overall time used for the function call.
+  optional int32 latency_ms = 2;
+
+  // Time used to persist the blob store.
+  optional int32 blob_store_persist_latency_ms = 3;
+
+  // Time used to persist the document store.
+  optional int32 document_store_total_persist_latency_ms = 4;
+
+  // Time used to persist the document store components.
+  optional int32 document_store_components_persist_latency_ms = 5;
+
+  // Time used to update the document store checksum.
+  optional int32 document_store_checksum_update_latency_ms = 6;
+
+  // Time used to update the document log checksum.
+  optional int32 document_log_checksum_update_latency_ms = 7;
+
+  // Time used to sync the document log data to disk.
+  optional int32 document_log_data_sync_latency_ms = 8;
+
+  // Time used to persist the schema store. If the persist type is
+  // RECOVERY_PROOF, this will update the checksums of the schema store. If the
+  // persist type is FULL, this will update the checksums internally and
+  // fdatasync/msync.
+  optional int32 schema_store_persist_latency_ms = 9;
+
+  // Time used to persist the index. If the persist type is RECOVERY_PROOF, this
+  // will update the checksums of the index.  If the persist type is FULL, this
+  // will update the checksums internally and fdatasync/msync.
+  optional int32 index_persist_latency_ms = 10;
+
+  // Time used to persist the integer index. If the persist type is
+  // RECOVERY_PROOF, this will update the checksums of the integer index. If the
+  // persist type is FULL, this will update the checksums internally and
+  // fdatasync/msync.
+  optional int32 integer_index_persist_latency_ms = 11;
+
+  // Time used to persist the qualified id join index.  If the persist type is
+  // RECOVERY_PROOF, this will update the checksums of the qualified id join
+  // index. If the persist type is FULL, this will update the checksums
+  // internally and fdatasync/msync.
+  optional int32 qualified_id_join_index_persist_latency_ms = 12;
+
+  // Time used to persist the embedding index. If the persist type is
+  // RECOVERY_PROOF, this will update the checksums of the embedding index. If
+  // the persist type is FULL, this will update the checksums internally and
+  // fdatasync/msync.
+  optional int32 embedding_index_persist_latency_ms = 13;
 }
diff --git a/proto/icing/proto/reset.proto b/proto/icing/proto/reset.proto
index 5e8b9f5..0bc22f3 100644
--- a/proto/icing/proto/reset.proto
+++ b/proto/icing/proto/reset.proto
@@ -20,11 +20,10 @@
 
 option java_package = "com.google.android.icing.proto";
 option java_multiple_files = true;
-
 option objc_class_prefix = "ICNG";
 
 // Result of a call to IcingSearchEngine.Reset
-// Next tag: 2
+// Next tag: 4
 message ResetResultProto {
   // Status code can be one of:
   //   OK
@@ -33,4 +32,11 @@
   //
   // See status.proto for more details.
   optional StatusProto status = 1;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 2;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 3;
 }
diff --git a/proto/icing/proto/schema.proto b/proto/icing/proto/schema.proto
index 1f7eb4b..2d3f1b7 100644
--- a/proto/icing/proto/schema.proto
+++ b/proto/icing/proto/schema.proto
@@ -16,6 +16,7 @@
 
 package icing.lib;
 
+import "icing/proto/persist.proto";
 import "icing/proto/status.proto";
 import "icing/proto/term.proto";
 
@@ -34,7 +35,7 @@
 // TODO(cassiewang) Define a sample proto file that can be used by tests and for
 // documentation.
 //
-// Next tag: 9
+// Next tag: 10
 message SchemaTypeConfigProto {
   // REQUIRED: Named type that identifies the structured, logical schema being
   // defined.
@@ -83,6 +84,10 @@
   // TypePropertyMask.
   repeated string parent_types = 6;
 
+  // List of all account properties that need to be managed. All accounts under
+  // this property should be existed in the account manager.
+  repeated string account_properties = 9;
+
   reserved 2, 3;
 }
 
@@ -455,7 +460,7 @@
 }
 
 // Result of a call to IcingSearchEngine.SetSchema
-// Next tag: 9
+// Next tag: 19
 message SetSchemaResultProto {
   // Status code can be one of:
   //   OK
@@ -493,7 +498,9 @@
   repeated string index_incompatible_changed_schema_types = 6;
 
   // Overall time used for the function call.
-  optional int32 latency_ms = 7;
+  //
+  // DEPRECATED: Use SetSchemaStatsProto.overall_latency_ms instead.
+  optional int32 latency_ms = 7 [deprecated = true];
 
   // Schema types that were changed in a way that was backwards compatible, but
   // invalidated the joinable cache.
@@ -502,10 +509,77 @@
   // but changed to joinable in the new definition. In this case, this property
   // will be considered join incompatible when setting new schema.
   repeated string join_incompatible_changed_schema_types = 8;
+
+  // Number of documents that were deleted due to the new schema with
+  // ignore_errors_and_delete_documents enabled.
+  optional int32 deleted_document_count = 9;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 10;
+
+  // Schema types that were changed in a way that was backwards compatible, but
+  // invalidated the scorable cache.
+  repeated string scorable_property_incompatible_changed_schema_types = 11;
+
+  // Stats and latencies for the SetSchema call.
+  optional SetSchemaStatsProto set_schema_stats = 12;
+
+  // Whether the term index (lite and main indices) was restored as a result of
+  // the set schema call.
+  optional bool has_term_index_restored = 13;
+
+  // Whether the integer index was restored as a result of the set schema call.
+  optional bool has_integer_index_restored = 14;
+
+  // Whether the embedding index was restored as a result of the set schema
+  // call.
+  optional bool has_embedding_index_restored = 15;
+
+  // Whether the qualified id join index was restored as a result of the set
+  // schema call.
+  optional bool has_qualified_id_join_index_restored = 16;
+
+  // Persist type needed after setting the new schema. If UNKNOWN, then
+  // everything is clean and no need to call persistToDisk.
+  optional PersistType.Code needs_persist_type = 17;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 18;
+}
+
+// Stats and latencies for IcingSearchEngine::SetSchema.
+// Next tag: 7
+message SetSchemaStatsProto {
+  // Overall time used for the function call. This is the sum of the latencies
+  // recorded in the other fields (schema_store_set_schema_latency,
+  // document_store_update_schema_latency,
+  // document_store_optimized_update_schema_latency,
+  // index_restoration_latency,
+  // scorable_property_cache_regeneration_latency)
+  optional int32 overall_latency_ms = 1;
+
+  // Latency for setting the schema in the schema store.
+  optional int32 schema_store_set_schema_latency_ms = 2;
+
+  // Latency for updating the document store's derived data using
+  // DocumentStore::UpdateSchemaStore.
+  optional int32 document_store_update_schema_latency_ms = 3;
+
+  // Latency for updating the document store's derived data using
+  // DocumentStore::OptimizedUpdateSchemaStore.
+  optional int32 document_store_optimized_update_schema_latency_ms = 4;
+
+  // Latency for rebuilding the index following an index-incompatible schema
+  // change.
+  optional int32 index_restoration_latency_ms = 5;
+
+  // Latency for regenerating the scorable property cache.
+  optional int32 scorable_property_cache_regeneration_latency_ms = 6;
 }
 
 // Result of a call to IcingSearchEngine.GetSchema
-// Next tag: 3
+// Next tag: 4
 message GetSchemaResultProto {
   // Status code can be one of:
   //   OK
@@ -522,10 +596,13 @@
   // Copy of the Schema proto. Modifying this does not affect the Schema that
   // IcingSearchEngine holds.
   optional SchemaProto schema = 2;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 3;
 }
 
 // Result of a call to IcingSearchEngine.GetSchemaType
-// Next tag: 3
+// Next tag: 5
 message GetSchemaTypeResultProto {
   // Status code can be one of:
   //   OK
@@ -543,4 +620,11 @@
   // Modifying this does not affect the SchemaTypeConfig that IcingSearchEngine
   // holds.
   optional SchemaTypeConfigProto schema_type_config = 2;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 3;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 4;
 }
diff --git a/proto/icing/proto/search.proto b/proto/icing/proto/search.proto
index f0f6c5c..d6f5665 100644
--- a/proto/icing/proto/search.proto
+++ b/proto/icing/proto/search.proto
@@ -382,7 +382,7 @@
 }
 
 // Icing lib-supplied results from a search results.
-// Next tag: 6
+// Next tag: 9
 message SearchResultProto {
   // Status code can be one of:
   //   OK
@@ -442,6 +442,21 @@
 
   // Stats for query execution performance.
   optional QueryStatsProto query_stats = 5;
+
+  // This field is used only in the GetNextPage API.
+  //
+  // Whether GetNextPage returning an empty result is due to page token not
+  // found (mostly caused by cache eviction). If the client needs the entire set
+  // of result documents (as their ground truth), then they should restart the
+  // query via Search API when this field is true.
+  optional bool page_token_not_found = 6;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 7;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 8;
 }
 
 // Next tag: 3
@@ -459,12 +474,23 @@
   repeated string paths = 2;
 }
 
-// Next tag: 2
+// Next tag: 5
+// TODO(b/394875109): Rename it to GetRequestProto to be consistent with the
+// name in AppSearch.
 message GetResultSpecProto {
+  optional string namespace_requested = 2;
+  repeated string ids = 3;
+
   // How to specify a subset of properties to retrieve. If no type property mask
   // has been specified for a schema type, then *all* properties of that schema
   // type will be retrieved.
   repeated TypePropertyMask type_property_masks = 1;
+
+  // The maximum number of accumulated bytes for the documents to return in the
+  // result. This limit is to prevent the result from being too large, and we
+  // can't send it over VM boundary, which has transaction limit 600KB.
+  optional int32 num_total_document_bytes_to_return = 4
+      [default = 2147483647];  // INT_MAX
 }
 
 // Next tag: 12
@@ -541,7 +567,7 @@
   repeated string document_uris = 2;
 }
 
-// Next tag: 3
+// Next tag: 5
 message SuggestionResponse {
   message Suggestion {
     // The suggested query string for client to search for.
@@ -557,6 +583,13 @@
   optional StatusProto status = 1;
 
   repeated Suggestion suggestions = 2;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 3;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 4;
 }
 
 // Specification for a left outer join.
@@ -627,3 +660,23 @@
   }
   optional AggregationScoringStrategy.Code aggregation_scoring_strategy = 5;
 }
+
+// Next tag: 3
+message GetNextPageRequestProto {
+  // REQUIRED: The next page token to use for retrieving the next page of
+  // results.
+  //
+  // This is a token returned from a previous Search or GetNextPage
+  // call. The retrieved SearchResultProto will be empty if this token is
+  // invalid (value of 0, or previously invalidated by Icing)
+  optional int64 next_page_token = 1;
+
+  // The max number of results to return from the page.
+  //
+  // This is only an upper limit on the number of results to fetch from the
+  // page. Fewer results may be returned if there are not enough results
+  // remaining in the page, or if other limits (e.g. page size limit) are
+  // reached.
+  optional int32 max_results_to_retrieve_from_page = 2
+      [default = 2147483647];  // INT_MAX
+}
diff --git a/proto/icing/proto/status.proto b/proto/icing/proto/status.proto
index 06ec6c4..4b97e91 100644
--- a/proto/icing/proto/status.proto
+++ b/proto/icing/proto/status.proto
@@ -18,13 +18,12 @@
 
 option java_package = "com.google.android.icing.proto";
 option java_multiple_files = true;
-
 option objc_class_prefix = "ICNG";
 
 // Canonical status to indicate the results of API calls.
 // Next tag: 3
 message StatusProto {
-  // Next tag: 10
+  // Next tag: 11
   enum Code {
     // A default for all other use-cases. Should never be used in practice. This
     // may happen if there are backwards-compatibility issues.
@@ -71,6 +70,9 @@
     // same name within a type.
     ALREADY_EXISTS = 9;
 
+    // The system is unavailable to process the requested operation.
+    UNAVAILABLE = 10;
+
     // Any future status codes.
   }
   optional Code code = 1;
diff --git a/proto/icing/proto/storage.proto b/proto/icing/proto/storage.proto
index a9eb84a..6b5a5cd 100644
--- a/proto/icing/proto/storage.proto
+++ b/proto/icing/proto/storage.proto
@@ -200,7 +200,7 @@
   repeated NamespaceBlobStorageInfoProto namespace_blob_storage_info = 5;
 }
 
-// Next tag: 3
+// Next tag: 5
 message StorageInfoResultProto {
   // Status code can be one of:
   //   OK
@@ -211,4 +211,11 @@
 
   // Storage information of Icing.
   optional StorageInfoProto storage_info = 2;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 3;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 4;
 }
diff --git a/proto/icing/proto/usage.proto b/proto/icing/proto/usage.proto
index eaa2671..3e0da95 100644
--- a/proto/icing/proto/usage.proto
+++ b/proto/icing/proto/usage.proto
@@ -57,7 +57,7 @@
 }
 
 // Result of a call to IcingSearchEngine.ReportUsage
-// Next tag: 2
+// Next tag: 4
 message ReportUsageResultProto {
   // Status code can be one of:
   //   OK
@@ -66,4 +66,11 @@
   //
   // See status.proto for more details.
   optional StatusProto status = 1;
+
+  // The latency of the GetVm call.
+  optional int32 get_vm_latency_ms = 2;
+
+  // The start time of the binder transaction latency. This is used to measure
+  // the latency of passing data from the VM via the binder.
+  optional int64 vm_binder_transaction_latency_start_time_ms = 3;
 }
diff --git a/synced_AOSP_CL_number.txt b/synced_AOSP_CL_number.txt
index ba3548f..abe0202 100644
--- a/synced_AOSP_CL_number.txt
+++ b/synced_AOSP_CL_number.txt
@@ -1 +1 @@
-set(synced_AOSP_CL_number=733916508)
+set(synced_AOSP_CL_number=853041928)