Merge 25Q1 (ab/12770256) to aosp-main-future

Bug: 385190204
Merged-In: I3d1adcdbd8c83f278969072cf1efcd68e1b5f27d
Change-Id: Ib971cea2691b405d7f2cd472401dda952074371b
diff --git a/Android.bp b/Android.bp
index 0b038f5..f0a7b19 100644
--- a/Android.bp
+++ b/Android.bp
@@ -123,7 +123,8 @@
         "icing/query/**/*_test.cc",
         "icing/scoring/**/*_test.cc",
         "icing/store/*_test.cc",
-        "icing/testing/*test*.cc",
+        "icing/testing/*test.cc",
+        "icing/testing/*test-data.cc",
         "icing/tokenization/*_test.cc",
         "icing/util/*_test.cc",
     ],
diff --git a/build.gradle b/build.gradle
index 97cc5e1..38ae38d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -53,21 +53,6 @@
     }
 }
 
-protobuf {
-    protoc {
-        artifact = libs.protobufCompiler.get()
-    }
-    generateProtoTasks {
-        all().each { task ->
-            task.builtins {
-                java {
-                    option 'lite'
-                }
-            }
-        }
-    }
-}
-
 androidx {
     mavenVersion = LibraryVersions.APPSEARCH
 }
diff --git a/icing/feature-flags.h b/icing/feature-flags.h
new file mode 100644
index 0000000..63482f9
--- /dev/null
+++ b/icing/feature-flags.h
@@ -0,0 +1,56 @@
+// Copyright (C) 2024 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_FEATURE_FLAGS_H_
+#define ICING_FEATURE_FLAGS_H_
+
+namespace icing {
+namespace lib {
+
+class FeatureFlags {
+ public:
+  explicit FeatureFlags(bool enable_scorable_properties,
+                        bool enable_embedding_quantization,
+                        bool enable_repeated_field_joins)
+      : enable_scorable_properties_(enable_scorable_properties),
+        enable_embedding_quantization_(enable_embedding_quantization),
+        enable_repeated_field_joins_(enable_repeated_field_joins) {}
+
+  bool enable_scorable_properties() const {
+    return enable_scorable_properties_;
+  }
+
+  bool enable_embedding_quantization() const {
+    return enable_embedding_quantization_;
+  }
+
+  bool enable_repeated_field_joins() const {
+    return enable_repeated_field_joins_;
+  }
+
+ private:
+  bool enable_scorable_properties_;
+
+  // Whether to enable quantization for embedding vectors. If false, all
+  // embedding vectors will not be quantized. Otherwise, quantization will be
+  // controlled by the quantization type specified in the schema.
+  bool enable_embedding_quantization_;
+
+  bool enable_repeated_field_joins_;
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_FEATURE_FLAGS_H_
diff --git a/icing/file/file-backed-vector.h b/icing/file/file-backed-vector.h
index 72b7fe3..8bd7161 100644
--- a/icing/file/file-backed-vector.h
+++ b/icing/file/file-backed-vector.h
@@ -435,9 +435,9 @@
 
     int32_t size() const { return len_; }
 
-    // Set the mutable array slice (starting at idx) by the given element array.
-    // It handles SetDirty properly for the file-backed-vector when modifying
-    // elements.
+    // Sets the mutable array slice (starting at idx) by the given element
+    // array. It handles SetDirty properly for the file-backed-vector when
+    // modifying elements.
     //
     // REQUIRES: arr is valid && arr_len >= 0 && idx >= 0 && idx + arr_len <=
     //           size(), otherwise the behavior is undefined.
@@ -448,6 +448,19 @@
       }
     }
 
+    // Fills the mutable array slice, starting at idx with the given length, by
+    // the given value. It handles SetDirty properly for the file-backed-vector
+    // when modifying elements.
+    //
+    // REQUIRES: len >= 0 && idx >= 0 && idx + len <= size(), otherwise the
+    //           behavior is undefined.
+    void Fill(int32_t idx, int32_t len, const T& value) {
+      for (int32_t i = 0; i < len; ++i) {
+        SetDirty(idx + i);
+        data_[idx + i] = value;
+      }
+    }
+
    private:
     MutableArrayView(FileBackedVector<T>* vector, T* data, int32_t len)
         : vector_(vector),
diff --git a/icing/file/file-backed-vector_test.cc b/icing/file/file-backed-vector_test.cc
index a4f5bf8..4561e39 100644
--- a/icing/file/file-backed-vector_test.cc
+++ b/icing/file/file-backed-vector_test.cc
@@ -500,6 +500,96 @@
               ElementsAre(1, 1, 1, 1, 1));
 }
 
+TEST_F(FileBackedVectorTest, MutableArrayViewFill) {
+  // Create a vector and add some data.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<FileBackedVector<int>> vector,
+      FileBackedVector<int>::Create(
+          filesystem_, file_path_,
+          MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
+  Insert(vector.get(), /*idx=*/0, std::vector<int>(/*count=*/100, /*value=*/1));
+  EXPECT_THAT(vector->GetChecksum(), Eq(Crc32(2494890115U)));
+  EXPECT_THAT(vector->UpdateChecksum(), IsOkAndHolds(Crc32(2494890115U)));
+  EXPECT_THAT(vector->GetChecksum(), Eq(Crc32(2494890115U)));
+
+  constexpr int kArrayViewOffset = 3;
+  constexpr int kArrayViewLen = 5;
+  ICING_ASSERT_OK_AND_ASSIGN(
+      FileBackedVector<int>::MutableArrayView mutable_arr,
+      vector->GetMutable(kArrayViewOffset, kArrayViewLen));
+
+  mutable_arr.Fill(/*idx=*/0, /*len=*/3, 1234);
+  EXPECT_THAT(Get(vector.get(), kArrayViewOffset, kArrayViewLen),
+              ElementsAre(1234, 1234, 1234, 1, 1));
+
+  mutable_arr.Fill(/*idx=*/2, /*len=*/3, 5678);
+  EXPECT_THAT(Get(vector.get(), kArrayViewOffset, kArrayViewLen),
+              ElementsAre(1234, 1234, 5678, 5678, 5678));
+}
+
+TEST_F(FileBackedVectorTest, MutableArrayViewFillWithZeroLength) {
+  // Create a vector and add some data.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<FileBackedVector<int>> vector,
+      FileBackedVector<int>::Create(
+          filesystem_, file_path_,
+          MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
+  Insert(vector.get(), /*idx=*/0, std::vector<int>(/*count=*/100, /*value=*/1));
+  EXPECT_THAT(vector->GetChecksum(), Eq(Crc32(2494890115U)));
+  EXPECT_THAT(vector->UpdateChecksum(), IsOkAndHolds(Crc32(2494890115U)));
+  EXPECT_THAT(vector->GetChecksum(), Eq(Crc32(2494890115U)));
+
+  constexpr int kArrayViewOffset = 3;
+  constexpr int kArrayViewLen = 5;
+  ICING_ASSERT_OK_AND_ASSIGN(
+      FileBackedVector<int>::MutableArrayView mutable_arr,
+      vector->GetMutable(kArrayViewOffset, kArrayViewLen));
+
+  // Zero len should work and change nothing
+  mutable_arr.Fill(/*idx=*/0, /*len=*/0, 1234);
+  EXPECT_THAT(Get(vector.get(), kArrayViewOffset, kArrayViewLen),
+              ElementsAre(1, 1, 1, 1, 1));
+}
+
+TEST_F(FileBackedVectorTest, MutableArrayViewFillShouldSetDirty) {
+  // Create a vector and add some data.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<FileBackedVector<int>> vector,
+      FileBackedVector<int>::Create(
+          filesystem_, file_path_,
+          MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC));
+  Insert(vector.get(), /*idx=*/0, std::vector<int>(/*count=*/100, /*value=*/1));
+  EXPECT_THAT(vector->GetChecksum(), Eq(Crc32(2494890115U)));
+  EXPECT_THAT(vector->UpdateChecksum(), IsOkAndHolds(Crc32(2494890115U)));
+  EXPECT_THAT(vector->GetChecksum(), Eq(Crc32(2494890115U)));
+
+  std::string_view reconstructed_view(
+      reinterpret_cast<const char*>(vector->array()),
+      vector->num_elements() * sizeof(int));
+
+  constexpr int kArrayViewOffset = 3;
+  constexpr int kArrayViewLen = 5;
+  ICING_ASSERT_OK_AND_ASSIGN(
+      FileBackedVector<int>::MutableArrayView mutable_arr,
+      vector->GetMutable(kArrayViewOffset, kArrayViewLen));
+
+  // Use Fill() to mutate elements
+  // MutableArrayView should set the affected element indices dirty so that
+  // UpdateChecksum() can pick up the change and compute the checksum correctly.
+  // Validate by mapping another array on top.
+  mutable_arr.Fill(/*idx=*/0, /*len=*/3, 1234);
+  ASSERT_THAT(Get(vector.get(), kArrayViewOffset, kArrayViewLen),
+              ElementsAre(1234, 1234, 1234, 1, 1));
+  ICING_ASSERT_OK_AND_ASSIGN(Crc32 crc1, vector->UpdateChecksum());
+  EXPECT_THAT(crc1, Eq(Crc32(reconstructed_view)));
+
+  mutable_arr.Fill(/*idx=*/2, /*len=*/3, 5678);
+  ASSERT_THAT(Get(vector.get(), kArrayViewOffset, kArrayViewLen),
+              ElementsAre(1234, 1234, 5678, 5678, 5678));
+  ICING_ASSERT_OK_AND_ASSIGN(Crc32 crc2, vector->UpdateChecksum());
+  EXPECT_THAT(crc2, Eq(Crc32(reconstructed_view)));
+}
+
 TEST_F(FileBackedVectorTest, MutableArrayViewIndexOperatorShouldSetDirty) {
   // Create an array with some data.
   ICING_ASSERT_OK_AND_ASSIGN(
diff --git a/icing/file/memory-mapped-file-backed-proto-log.h b/icing/file/memory-mapped-file-backed-proto-log.h
index a5a9d16..cc8f35f 100644
--- a/icing/file/memory-mapped-file-backed-proto-log.h
+++ b/icing/file/memory-mapped-file-backed-proto-log.h
@@ -15,6 +15,7 @@
 #ifndef ICING_FILE_MEMORY_MAPPED_FILE_BACKED_PROTO_LOG_H_
 #define ICING_FILE_MEMORY_MAPPED_FILE_BACKED_PROTO_LOG_H_
 
+#include <cinttypes>
 #include <cstdint>
 #include <cstring>
 #include <memory>
@@ -84,6 +85,22 @@
   //   INTERNAL_ERROR on IO error
   libtextclassifier3::StatusOr<Crc32> UpdateChecksum();
 
+  // Calculates and returns the disk usage in bytes. Rounds up to the nearest
+  // block size.
+  //
+  // Returns:
+  //   Disk usage on success
+  //   INTERNAL_ERROR on IO error
+  libtextclassifier3::StatusOr<int64_t> GetDiskUsage() const;
+
+  // Returns the file size of the all the elements held in the log. File size
+  // is in bytes. This excludes the size of the header of the log file.
+  //
+  // Returns:
+  //   File size on success
+  //   INTERNAL_ERROR on IO error
+  libtextclassifier3::StatusOr<int64_t> GetElementsFileSize() const;
+
   // Reads the proto at the given index.
   //
   // Returns:
@@ -160,6 +177,18 @@
 }
 
 template <typename ProtoT>
+libtextclassifier3::StatusOr<int64_t>
+MemoryMappedFileBackedProtoLog<ProtoT>::GetDiskUsage() const {
+  return proto_fbv_->GetDiskUsage();
+}
+
+template <typename ProtoT>
+libtextclassifier3::StatusOr<int64_t>
+MemoryMappedFileBackedProtoLog<ProtoT>::GetElementsFileSize() const {
+  return proto_fbv_->GetElementsFileSize();
+}
+
+template <typename ProtoT>
 libtextclassifier3::StatusOr<
     std::unique_ptr<MemoryMappedFileBackedProtoLog<ProtoT>>>
 MemoryMappedFileBackedProtoLog<ProtoT>::Create(const Filesystem& filesystem,
@@ -187,9 +216,10 @@
         IcingStringUtil::StringPrintf("Index, %d, is less than 0", index));
   }
   if (index + sizeof(ProtoMetadata) >= proto_fbv_->num_elements()) {
+    uint64_t upper_index = proto_fbv_->num_elements() - sizeof(ProtoMetadata);
     return absl_ports::OutOfRangeError(IcingStringUtil::StringPrintf(
-        "Index, %d, is greater/equal than the upper bound, %d", index,
-        proto_fbv_->num_elements() - sizeof(ProtoMetadata)));
+        "Index, %" PRId32 ", is greater/equal than the upper bound, %" PRIu64,
+        index,  upper_index));
   }
 
   ProtoMetadata proto_metadata;
diff --git a/icing/file/portable-file-backed-proto-log.h b/icing/file/portable-file-backed-proto-log.h
index faccc6e..81010e8 100644
--- a/icing/file/portable-file-backed-proto-log.h
+++ b/icing/file/portable-file-backed-proto-log.h
@@ -116,14 +116,14 @@
     explicit Options(
         bool compress_in,
         const int32_t max_proto_size_in = constants::kMaxProtoSize,
-        const int32_t compression_level_in = kDeflateCompressionLevel)
+        const int32_t compression_level_in = kDefaultCompressionLevel)
         : compress(compress_in),
           max_proto_size(max_proto_size_in),
           compression_level(compression_level_in) {}
   };
 
   // Level of compression, BEST_SPEED = 1, BEST_COMPRESSION = 9
-  static constexpr int kDeflateCompressionLevel = 3;
+  static constexpr int kDefaultCompressionLevel = 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
diff --git a/icing/file/portable-file-backed-proto-log_test.cc b/icing/file/portable-file-backed-proto-log_test.cc
index b564a2c..587bfdc 100644
--- a/icing/file/portable-file-backed-proto-log_test.cc
+++ b/icing/file/portable-file-backed-proto-log_test.cc
@@ -74,7 +74,7 @@
   std::string file_path_;
   bool compress_ = true;
   int32_t compression_level_ =
-      PortableFileBackedProtoLog<DocumentProto>::kDeflateCompressionLevel;
+      PortableFileBackedProtoLog<DocumentProto>::kDefaultCompressionLevel;
   int64_t max_proto_size_ = 256 * 1024;  // 256 KiB
 };
 
diff --git a/icing/file/version-util.cc b/icing/file/version-util.cc
index e750b0c..af5ea50 100644
--- a/icing/file/version-util.cc
+++ b/icing/file/version-util.cc
@@ -66,8 +66,7 @@
     // Real error.
     return std::move(existing_flash_index_magic).status();
   }
-  if (existing_flash_index_magic.ValueOrDie() ==
-      kVersionZeroFlashIndexMagic) {
+  if (existing_flash_index_magic.ValueOrDie() == kVersionZeroFlashIndexMagic) {
     existing_version_info.version = 0;
     if (existing_version_info.max_version == -1) {
       existing_version_info.max_version = 0;
@@ -233,7 +232,8 @@
         /*needs_schema_store_derived_files_rebuild=*/true,
         /*needs_term_index_rebuild=*/true,
         /*needs_integer_index_rebuild=*/true,
-        /*needs_qualified_id_join_index_rebuild=*/true);
+        /*needs_qualified_id_join_index_rebuild=*/true,
+        /*needs_embedding_index_rebuild=*/true);
   }
 
   // 2. Compare the previous enabled features with the current enabled features
@@ -263,7 +263,8 @@
           /*needs_schema_store_derived_files_rebuild=*/true,
           /*needs_term_index_rebuild=*/true,
           /*needs_integer_index_rebuild=*/true,
-          /*needs_qualified_id_join_index_rebuild=*/true);
+          /*needs_qualified_id_join_index_rebuild=*/true,
+          /*needs_embedding_index_rebuild=*/true);
     }
     if (curr_features.find(prev_feature) == curr_features.end()) {
       DerivedFilesRebuildResult required_rebuilds =
@@ -318,6 +319,10 @@
         // version 3 -> version 4 upgrade, no need to rebuild
         break;
       }
+      case 4: {
+        // version 4 -> version 5 upgrade, no need to rebuild
+        break;
+      }
       default:
         // This should not happen. Rebuild anyway if unsure.
         should_rebuild |= true;
@@ -330,13 +335,62 @@
 DerivedFilesRebuildResult GetFeatureDerivedFilesRebuildResult(
     IcingSearchEngineFeatureInfoProto::FlaggedFeatureType feature) {
   switch (feature) {
+    case IcingSearchEngineFeatureInfoProto::FEATURE_SCORABLE_PROPERTIES: {
+      return DerivedFilesRebuildResult(
+          /*needs_document_store_derived_files_rebuild=*/true,
+          /*needs_schema_store_derived_files_rebuild=*/false,
+          /*needs_term_index_rebuild=*/false,
+          /*needs_integer_index_rebuild=*/false,
+          /*needs_qualified_id_join_index_rebuild=*/false,
+          /*needs_embedding_index_rebuild=*/false);
+    }
     case IcingSearchEngineFeatureInfoProto::FEATURE_HAS_PROPERTY_OPERATOR: {
       return DerivedFilesRebuildResult(
           /*needs_document_store_derived_files_rebuild=*/false,
           /*needs_schema_store_derived_files_rebuild=*/false,
           /*needs_term_index_rebuild=*/true,
           /*needs_integer_index_rebuild=*/false,
-          /*needs_qualified_id_join_index_rebuild=*/false);
+          /*needs_qualified_id_join_index_rebuild=*/false,
+          /*needs_embedding_index_rebuild=*/false);
+    }
+    case IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_INDEX: {
+      return DerivedFilesRebuildResult(
+          /*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=*/false,
+          /*needs_embedding_index_rebuild=*/true);
+    }
+    case IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_QUANTIZATION: {
+      return DerivedFilesRebuildResult(
+          /*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=*/false,
+          /*needs_embedding_index_rebuild=*/true);
+    }
+    case IcingSearchEngineFeatureInfoProto::FEATURE_SCHEMA_DATABASE: {
+      // The schema database feature requires schema-store migration, which is
+      // done separately from derived files rebuild.
+      return DerivedFilesRebuildResult(
+          /*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=*/false,
+          /*needs_embedding_index_rebuild=*/false);
+    }
+    case IcingSearchEngineFeatureInfoProto::
+        FEATURE_QUALIFIED_ID_JOIN_INDEX_V3_AND_DELETE_PROPAGATE_FROM: {
+      return DerivedFilesRebuildResult(
+          /*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::UNKNOWN:
       return DerivedFilesRebuildResult(
@@ -344,10 +398,27 @@
           /*needs_schema_store_derived_files_rebuild=*/true,
           /*needs_term_index_rebuild=*/true,
           /*needs_integer_index_rebuild=*/true,
-          /*needs_qualified_id_join_index_rebuild=*/true);
+          /*needs_qualified_id_join_index_rebuild=*/true,
+          /*needs_embedding_index_rebuild=*/true);
   }
 }
 
+bool SchemaDatabaseMigrationRequired(
+    const IcingSearchEngineVersionProto& prev_version_proto) {
+  if (prev_version_proto.version() < kSchemaDatabaseVersion) {
+    return true;
+  }
+  for (const auto& feature : prev_version_proto.enabled_features()) {
+    // The schema database feature was enabled in the previous version, so no
+    // need to migrate.
+    if (feature.feature_type() ==
+        IcingSearchEngineFeatureInfoProto::FEATURE_SCHEMA_DATABASE) {
+      return false;
+    }
+  }
+  return true;
+}
+
 IcingSearchEngineFeatureInfoProto GetFeatureInfoProto(
     IcingSearchEngineFeatureInfoProto::FlaggedFeatureType feature) {
   IcingSearchEngineFeatureInfoProto info;
@@ -363,6 +434,7 @@
   info.set_needs_integer_index_rebuild(result.needs_integer_index_rebuild);
   info.set_needs_qualified_id_join_index_rebuild(
       result.needs_qualified_id_join_index_rebuild);
+  info.set_needs_embedding_index_rebuild(result.needs_embedding_index_rebuild);
 
   return info;
 }
@@ -375,6 +447,31 @@
     enabled_features->Add(GetFeatureInfoProto(
         IcingSearchEngineFeatureInfoProto::FEATURE_HAS_PROPERTY_OPERATOR));
   }
+  // EmbeddingIndex feature
+  if (options.enable_embedding_index()) {
+    enabled_features->Add(GetFeatureInfoProto(
+        IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_INDEX));
+  }
+  if (options.enable_scorable_properties()) {
+    enabled_features->Add(GetFeatureInfoProto(
+        IcingSearchEngineFeatureInfoProto::FEATURE_SCORABLE_PROPERTIES));
+  }
+  // EmbeddingQuantization feature
+  if (options.enable_embedding_quantization()) {
+    enabled_features->Add(GetFeatureInfoProto(
+        IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_QUANTIZATION));
+  }
+  // SchemaDatabase feature
+  if (options.enable_schema_database()) {
+    enabled_features->Add(GetFeatureInfoProto(
+        IcingSearchEngineFeatureInfoProto::FEATURE_SCHEMA_DATABASE));
+  }
+  // QualifiedIdJoinIndex V3 and delete propagation type PROPAGATE_FROM feature
+  if (options.enable_qualified_id_join_index_v3_and_delete_propagate_from()) {
+    enabled_features->Add(GetFeatureInfoProto(
+        IcingSearchEngineFeatureInfoProto::
+            FEATURE_QUALIFIED_ID_JOIN_INDEX_V3_AND_DELETE_PROPAGATE_FROM));
+  }
 }
 
 }  // namespace version_util
diff --git a/icing/file/version-util.h b/icing/file/version-util.h
index 7e93b5c..0f5e8a4 100644
--- a/icing/file/version-util.h
+++ b/icing/file/version-util.h
@@ -37,18 +37,20 @@
 //   (There were no M-2023-10, M-2023-12).
 // - 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.
-//
-// TODO(b/314816301): Bump kVersion to 4 for Android V rollout with v2 version
-// detection
-inline static constexpr int32_t kVersion = 4;
+// - Version 5: M-2025-02. Schema is compatible with v1, v2, v3 and v4.
+inline static constexpr int32_t kVersion = 5;
 inline static constexpr int32_t kVersionOne = 1;
 inline static constexpr int32_t kVersionTwo = 2;
 inline static constexpr int32_t kVersionThree = 3;
 inline static constexpr int32_t kVersionFour = 4;
+inline static constexpr int32_t kVersionFive = 5;
 
 // Version at which v2 version file is introduced.
 inline static constexpr int32_t kFirstV2Version = kVersionFour;
 
+// Version at which the database field is introduced to the schema proto.
+inline static constexpr int32_t kSchemaDatabaseVersion = kVersionFive;
+
 inline static constexpr int kVersionZeroFlashIndexMagic = 0x6dfba6ae;
 
 inline static constexpr std::string_view kVersionFilenameV1 = "version";
@@ -95,6 +97,7 @@
   bool needs_term_index_rebuild = false;
   bool needs_integer_index_rebuild = false;
   bool needs_qualified_id_join_index_rebuild = false;
+  bool needs_embedding_index_rebuild = false;
 
   DerivedFilesRebuildResult() = default;
 
@@ -102,7 +105,8 @@
       bool needs_document_store_derived_files_rebuild_in,
       bool needs_schema_store_derived_files_rebuild_in,
       bool needs_term_index_rebuild_in, bool needs_integer_index_rebuild_in,
-      bool needs_qualified_id_join_index_rebuild_in)
+      bool needs_qualified_id_join_index_rebuild_in,
+      bool needs_embedding_index_rebuild_in)
       : needs_document_store_derived_files_rebuild(
             needs_document_store_derived_files_rebuild_in),
         needs_schema_store_derived_files_rebuild(
@@ -110,13 +114,15 @@
         needs_term_index_rebuild(needs_term_index_rebuild_in),
         needs_integer_index_rebuild(needs_integer_index_rebuild_in),
         needs_qualified_id_join_index_rebuild(
-            needs_qualified_id_join_index_rebuild_in) {}
+            needs_qualified_id_join_index_rebuild_in),
+        needs_embedding_index_rebuild(needs_embedding_index_rebuild_in) {}
 
   bool IsRebuildNeeded() const {
     return needs_document_store_derived_files_rebuild ||
            needs_schema_store_derived_files_rebuild ||
            needs_term_index_rebuild || needs_integer_index_rebuild ||
-           needs_qualified_id_join_index_rebuild;
+           needs_qualified_id_join_index_rebuild ||
+           needs_embedding_index_rebuild;
   }
 
   bool operator==(const DerivedFilesRebuildResult& other) const {
@@ -127,23 +133,20 @@
            needs_term_index_rebuild == other.needs_term_index_rebuild &&
            needs_integer_index_rebuild == other.needs_integer_index_rebuild &&
            needs_qualified_id_join_index_rebuild ==
-               other.needs_qualified_id_join_index_rebuild;
+               other.needs_qualified_id_join_index_rebuild &&
+           needs_embedding_index_rebuild == other.needs_embedding_index_rebuild;
   }
 
   void CombineWithOtherRebuildResultOr(const DerivedFilesRebuildResult& other) {
-    needs_document_store_derived_files_rebuild =
-        needs_document_store_derived_files_rebuild ||
+    needs_document_store_derived_files_rebuild |=
         other.needs_document_store_derived_files_rebuild;
-    needs_schema_store_derived_files_rebuild =
-        needs_schema_store_derived_files_rebuild ||
+    needs_schema_store_derived_files_rebuild |=
         other.needs_schema_store_derived_files_rebuild;
-    needs_term_index_rebuild =
-        needs_term_index_rebuild || other.needs_term_index_rebuild;
-    needs_integer_index_rebuild =
-        needs_integer_index_rebuild || other.needs_integer_index_rebuild;
-    needs_qualified_id_join_index_rebuild =
-        needs_qualified_id_join_index_rebuild ||
+    needs_term_index_rebuild |= other.needs_term_index_rebuild;
+    needs_integer_index_rebuild |= other.needs_integer_index_rebuild;
+    needs_qualified_id_join_index_rebuild |=
         other.needs_qualified_id_join_index_rebuild;
+    needs_embedding_index_rebuild |= other.needs_embedding_index_rebuild;
   }
 };
 
@@ -167,7 +170,6 @@
   return VersionInfo(version_proto.version(), version_proto.max_version());
 }
 
-
 // Reads the IcingSearchEngineVersionProto from the version files of the
 // existing data.
 //
@@ -247,6 +249,14 @@
 bool ShouldRebuildDerivedFiles(const VersionInfo& existing_version_info,
                                int32_t curr_version = kVersion);
 
+// Returns whether the schema database migration is required.
+//
+// This is true if the previous version is less than the version at which the
+// database field is introduced, or if the schema database feature was
+// not enabled in the previous version.
+bool SchemaDatabaseMigrationRequired(
+    const IcingSearchEngineVersionProto& prev_version_proto);
+
 // Returns the derived files rebuilds required for a given feature.
 DerivedFilesRebuildResult GetFeatureDerivedFilesRebuildResult(
     IcingSearchEngineFeatureInfoProto::FlaggedFeatureType feature);
diff --git a/icing/file/version-util_test.cc b/icing/file/version-util_test.cc
index f2922e2..d881adc 100644
--- a/icing/file/version-util_test.cc
+++ b/icing/file/version-util_test.cc
@@ -525,7 +525,55 @@
         VersionUtilStateChangeTestParam(
             /*existing_version_info_in=*/VersionInfo(3, 4),
             /*curr_version_in=*/2,
-            /*expected_state_change_in=*/StateChange::kRollBack)));
+            /*expected_state_change_in=*/StateChange::kRollBack),
+
+        // - version 3, max_version 3
+        // - Current version = 4
+        // - Result: upgrade
+        VersionUtilStateChangeTestParam(
+            /*existing_version_info_in=*/VersionInfo(3, 3),
+            /*curr_version_in=*/4,
+            /*expected_state_change_in=*/StateChange::kUpgrade),
+
+        // - version 4, max_version 4
+        // - Current version = 3
+        // - Result: rollback
+        VersionUtilStateChangeTestParam(
+            /*existing_version_info_in=*/VersionInfo(4, 4),
+            /*curr_version_in=*/3,
+            /*expected_state_change_in=*/StateChange::kRollBack),
+
+        // - version 4, max_version 5
+        // - Current version = 4
+        // - Result: compatible
+        VersionUtilStateChangeTestParam(
+            /*existing_version_info_in=*/VersionInfo(4, 5),
+            /*curr_version_in=*/4,
+            /*expected_state_change_in=*/StateChange::kCompatible),
+
+        // - version 3, max_version 4
+        // - Current version = 5
+        // - Result: rollforward
+        VersionUtilStateChangeTestParam(
+            /*existing_version_info_in=*/VersionInfo(3, 4),
+            /*curr_version_in=*/5,
+            /*expected_state_change_in=*/StateChange::kRollForward),
+
+        // - version 3, max_version 3
+        // - Current version = 5
+        // - Result: upgrade
+        VersionUtilStateChangeTestParam(
+            /*existing_version_info_in=*/VersionInfo(3, 3),
+            /*curr_version_in=*/5,
+            /*expected_state_change_in=*/StateChange::kUpgrade),
+
+        // - version 4, max_version 5
+        // - Current version = 5
+        // - Result: rollforward
+        VersionUtilStateChangeTestParam(
+            /*existing_version_info_in=*/VersionInfo(4, 5),
+            /*curr_version_in=*/5,
+            /*expected_state_change_in=*/StateChange::kRollForward)));
 
 struct VersionUtilDerivedFilesRebuildTestParam {
   int32_t existing_version;
@@ -588,11 +636,12 @@
             /*curr_enabled_features_in=*/{},
             /*expected_derived_files_rebuild_result_in=*/
             DerivedFilesRebuildResult(
-                /*needs_document_store_derived_files_rebuild=*/true,
-                /*needs_schema_store_derived_files_rebuild=*/true,
-                /*needs_term_index_rebuild=*/true,
-                /*needs_integer_index_rebuild=*/true,
-                /*needs_qualified_id_join_index_rebuild=*/true)),
+                /*needs_document_store_derived_files_rebuild_in=*/true,
+                /*needs_schema_store_derived_files_rebuild_in=*/true,
+                /*needs_term_index_rebuild_in=*/true,
+                /*needs_integer_index_rebuild_in=*/true,
+                /*needs_qualified_id_join_index_rebuild_in=*/true,
+                /*needs_embedding_index_rebuild_in=*/true)),
 
         // - Existing version -1, max_version 2 (invalid)
         // - Existing enabled features = {}
@@ -606,11 +655,12 @@
             /*curr_enabled_features_in=*/{},
             /*expected_derived_files_rebuild_result_in=*/
             DerivedFilesRebuildResult(
-                /*needs_document_store_derived_files_rebuild=*/true,
-                /*needs_schema_store_derived_files_rebuild=*/true,
-                /*needs_term_index_rebuild=*/true,
-                /*needs_integer_index_rebuild=*/true,
-                /*needs_qualified_id_join_index_rebuild=*/true)),
+                /*needs_document_store_derived_files_rebuild_in=*/true,
+                /*needs_schema_store_derived_files_rebuild_in=*/true,
+                /*needs_term_index_rebuild_in=*/true,
+                /*needs_integer_index_rebuild_in=*/true,
+                /*needs_qualified_id_join_index_rebuild_in=*/true,
+                /*needs_embedding_index_rebuild_in=*/true)),
 
         // - Existing version 3, max_version 3 (pre v2 version check)
         // - Existing enabled features = {}
@@ -624,11 +674,12 @@
             /*curr_enabled_features_in=*/{},
             /*expected_derived_files_rebuild_result_in=*/
             DerivedFilesRebuildResult(
-                /*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=*/false)),
+                /*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=*/false,
+                /*needs_embedding_index_rebuild_in=*/false)),
 
         // - Existing version 3, max_version 3 (pre v2 version check)
         // - Existing enabled features = {}
@@ -643,11 +694,32 @@
             {IcingSearchEngineFeatureInfoProto::FEATURE_HAS_PROPERTY_OPERATOR},
             /*expected_derived_files_rebuild_result_in=*/
             DerivedFilesRebuildResult(
-                /*needs_document_store_derived_files_rebuild=*/false,
-                /*needs_schema_store_derived_files_rebuild=*/false,
-                /*needs_term_index_rebuild=*/true,
-                /*needs_integer_index_rebuild=*/false,
-                /*needs_qualified_id_join_index_rebuild=*/false)),
+                /*needs_document_store_derived_files_rebuild_in=*/false,
+                /*needs_schema_store_derived_files_rebuild_in=*/false,
+                /*needs_term_index_rebuild_in=*/true,
+                /*needs_integer_index_rebuild_in=*/false,
+                /*needs_qualified_id_join_index_rebuild_in=*/false,
+                /*needs_embedding_index_rebuild_in=*/false)),
+
+        // - Existing version 3, max_version 3 (pre v2 version check)
+        // - Existing enabled features = {}
+        // - Current version = 4
+        // - Current enabled features = {FEATURE_EMBEDDING_INDEX}
+        //
+        // - Result: rebuild embedding index
+        VersionUtilDerivedFilesRebuildTestParam(
+            /*existing_version_in=*/3, /*max_version_in=*/3,
+            /*existing_enabled_features_in=*/{}, /*curr_version_in=*/4,
+            /*curr_enabled_features_in=*/
+            {IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_INDEX},
+            /*expected_derived_files_rebuild_result_in=*/
+            DerivedFilesRebuildResult(
+                /*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=*/false,
+                /*needs_embedding_index_rebuild_in=*/true)),
 
         // - Existing version 4, max_version 4
         // - Existing enabled features = {}
@@ -661,11 +733,31 @@
             /*curr_enabled_features_in=*/{},
             /*expected_derived_files_rebuild_result_in=*/
             DerivedFilesRebuildResult(
-                /*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=*/false)),
+                /*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=*/false,
+                /*needs_embedding_index_rebuild_in=*/false)),
+
+        // - Existing version 4, max_version 4
+        // - Existing enabled features = {}
+        // - Current version = 5
+        // - Current enabled features = {}
+        //
+        // - Result: 4 -> 5 upgrade -- don't rebuild anything
+        VersionUtilDerivedFilesRebuildTestParam(
+            /*existing_version_in=*/4, /*max_version_in=*/4,
+            /*existing_enabled_features_in=*/{}, /*curr_version_in=*/5,
+            /*curr_enabled_features_in=*/{},
+            /*expected_derived_files_rebuild_result_in=*/
+            DerivedFilesRebuildResult(
+                /*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=*/false,
+                /*needs_embedding_index_rebuild_in=*/false)),
 
         // - Existing version 4, max_version 5
         // - Existing enabled features = {}
@@ -679,11 +771,12 @@
             /*curr_enabled_features_in=*/{},
             /*expected_derived_files_rebuild_result_in=*/
             DerivedFilesRebuildResult(
-                /*needs_document_store_derived_files_rebuild=*/true,
-                /*needs_schema_store_derived_files_rebuild=*/true,
-                /*needs_term_index_rebuild=*/true,
-                /*needs_integer_index_rebuild=*/true,
-                /*needs_qualified_id_join_index_rebuild=*/true)),
+                /*needs_document_store_derived_files_rebuild_in=*/true,
+                /*needs_schema_store_derived_files_rebuild_in=*/true,
+                /*needs_term_index_rebuild_in=*/true,
+                /*needs_integer_index_rebuild_in=*/true,
+                /*needs_qualified_id_join_index_rebuild_in=*/true,
+                /*needs_embedding_index_rebuild_in=*/true)),
 
         // - Existing version 5, max_version 5
         // - Existing enabled features = {}
@@ -697,11 +790,12 @@
             /*curr_enabled_features_in=*/{},
             /*expected_derived_files_rebuild_result_in=*/
             DerivedFilesRebuildResult(
-                /*needs_document_store_derived_files_rebuild=*/true,
-                /*needs_schema_store_derived_files_rebuild=*/true,
-                /*needs_term_index_rebuild=*/true,
-                /*needs_integer_index_rebuild=*/true,
-                /*needs_qualified_id_join_index_rebuild=*/true)),
+                /*needs_document_store_derived_files_rebuild_in=*/true,
+                /*needs_schema_store_derived_files_rebuild_in=*/true,
+                /*needs_term_index_rebuild_in=*/true,
+                /*needs_integer_index_rebuild_in=*/true,
+                /*needs_qualified_id_join_index_rebuild_in=*/true,
+                /*needs_embedding_index_rebuild_in=*/true)),
 
         // - Existing version 4, max_version 4
         // - Existing enabled features = {}
@@ -716,11 +810,78 @@
             {IcingSearchEngineFeatureInfoProto::FEATURE_HAS_PROPERTY_OPERATOR},
             /*expected_derived_files_rebuild_result_in=*/
             DerivedFilesRebuildResult(
-                /*needs_document_store_derived_files_rebuild=*/false,
-                /*needs_schema_store_derived_files_rebuild=*/false,
-                /*needs_term_index_rebuild=*/true,
-                /*needs_integer_index_rebuild=*/false,
-                /*needs_qualified_id_join_index_rebuild=*/false)),
+                /*needs_document_store_derived_files_rebuild_in=*/false,
+                /*needs_schema_store_derived_files_rebuild_in=*/false,
+                /*needs_term_index_rebuild_in=*/true,
+                /*needs_integer_index_rebuild_in=*/false,
+                /*needs_qualified_id_join_index_rebuild_in=*/false,
+                /*needs_embedding_index_rebuild_in=*/false)),
+
+        // - Existing version 4, max_version 4
+        // - Existing enabled features = {}
+        // - Current version = 4
+        // - Current enabled features = {FEATURE_EMBEDDING_INDEX}
+        //
+        // - Result: rebuild embedding index
+        VersionUtilDerivedFilesRebuildTestParam(
+            /*existing_version_in=*/4, /*max_version_in=*/4,
+            /*existing_enabled_features_in=*/{}, /*curr_version_in=*/4,
+            /*curr_enabled_features_in=*/
+            {IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_INDEX},
+            /*expected_derived_files_rebuild_result_in=*/
+            DerivedFilesRebuildResult(
+                /*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=*/false,
+                /*needs_embedding_index_rebuild_in=*/true)),
+
+        // - Existing version 4, max_version 4
+        // - Existing enabled features = {}
+        // - Current version = 4
+        // - Current enabled features = {FEATURE_EMBEDDING_INDEX,
+        //                               FEATURE_EMBEDDING_QUANTIZATION}
+        //
+        // - Result: rebuild embedding index
+        VersionUtilDerivedFilesRebuildTestParam(
+            /*existing_version_in=*/4, /*max_version_in=*/4,
+            /*existing_enabled_features_in=*/{}, /*curr_version_in=*/4,
+            /*curr_enabled_features_in=*/
+            {IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_INDEX,
+             IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_QUANTIZATION},
+            /*expected_derived_files_rebuild_result_in=*/
+            DerivedFilesRebuildResult(
+                /*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=*/false,
+                /*needs_embedding_index_rebuild_in=*/true)),
+
+        // - Existing version 4, max_version 4
+        // - Existing enabled features = {FEATURE_EMBEDDING_INDEX}
+        // - Current version = 4
+        // - Current enabled features = {FEATURE_EMBEDDING_INDEX,
+        //                               FEATURE_EMBEDDING_QUANTIZATION}
+        //
+        // - Result: rebuild embedding index
+        VersionUtilDerivedFilesRebuildTestParam(
+            /*existing_version_in=*/4, /*max_version_in=*/4,
+            /*existing_enabled_features_in=*/
+            {IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_INDEX},
+            /*curr_version_in=*/4,
+            /*curr_enabled_features_in=*/
+            {IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_INDEX,
+             IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_QUANTIZATION},
+            /*expected_derived_files_rebuild_result_in=*/
+            DerivedFilesRebuildResult(
+                /*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=*/false,
+                /*needs_embedding_index_rebuild_in=*/true)),
 
         // - Existing version 4, max_version 4
         // - Existing enabled features = {FEATURE_HAS_PROPERTY_OPERATOR}
@@ -735,11 +896,77 @@
             /*curr_version_in=*/4, /*curr_enabled_features_in=*/{},
             /*expected_derived_files_rebuild_result_in=*/
             DerivedFilesRebuildResult(
-                /*needs_document_store_derived_files_rebuild=*/false,
-                /*needs_schema_store_derived_files_rebuild=*/false,
-                /*needs_term_index_rebuild=*/true,
-                /*needs_integer_index_rebuild=*/false,
-                /*needs_qualified_id_join_index_rebuild=*/false)),
+                /*needs_document_store_derived_files_rebuild_in=*/false,
+                /*needs_schema_store_derived_files_rebuild_in=*/false,
+                /*needs_term_index_rebuild_in=*/true,
+                /*needs_integer_index_rebuild_in=*/false,
+                /*needs_qualified_id_join_index_rebuild_in=*/false,
+                /*needs_embedding_index_rebuild_in=*/false)),
+
+        // - Existing version 4, max_version 4
+        // - Existing enabled features = {FEATURE_EMBEDDING_INDEX}
+        // - Current version = 4
+        // - Current enabled features = {}
+        //
+        // - Result: rebuild embedding index
+        VersionUtilDerivedFilesRebuildTestParam(
+            /*existing_version_in=*/4, /*max_version_in=*/4,
+            /*existing_enabled_features_in=*/
+            {IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_INDEX},
+            /*curr_version_in=*/4, /*curr_enabled_features_in=*/{},
+            /*expected_derived_files_rebuild_result_in=*/
+            DerivedFilesRebuildResult(
+                /*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=*/false,
+                /*needs_embedding_index_rebuild_in=*/true)),
+
+        // - Existing version 4, max_version 4
+        // - Existing enabled features = {FEATURE_EMBEDDING_INDEX,
+        //                                FEATURE_EMBEDDING_QUANTIZATION}
+        // - Current version = 4
+        // - Current enabled features = {}
+        //
+        // - Result: rebuild embedding index
+        VersionUtilDerivedFilesRebuildTestParam(
+            /*existing_version_in=*/4, /*max_version_in=*/4,
+            /*existing_enabled_features_in=*/
+            {IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_INDEX,
+             IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_QUANTIZATION},
+            /*curr_version_in=*/4, /*curr_enabled_features_in=*/{},
+            /*expected_derived_files_rebuild_result_in=*/
+            DerivedFilesRebuildResult(
+                /*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=*/false,
+                /*needs_embedding_index_rebuild_in=*/true)),
+
+        // - Existing version 4, max_version 4
+        // - Existing enabled features = {FEATURE_EMBEDDING_INDEX,
+        //                                FEATURE_EMBEDDING_QUANTIZATION}
+        // - Current version = 4
+        // - Current enabled features = {FEATURE_EMBEDDING_INDEX}
+        //
+        // - Result: rebuild embedding index
+        VersionUtilDerivedFilesRebuildTestParam(
+            /*existing_version_in=*/4, /*max_version_in=*/4,
+            /*existing_enabled_features_in=*/
+            {IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_INDEX,
+             IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_QUANTIZATION},
+            /*curr_version_in=*/4, /*curr_enabled_features_in=*/
+            {IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_INDEX},
+            /*expected_derived_files_rebuild_result_in=*/
+            DerivedFilesRebuildResult(
+                /*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=*/false,
+                /*needs_embedding_index_rebuild_in=*/true)),
 
         // - Existing version 4, max_version 4
         // - Existing enabled features = {UNKNOWN}
@@ -755,11 +982,96 @@
             {IcingSearchEngineFeatureInfoProto::FEATURE_HAS_PROPERTY_OPERATOR},
             /*expected_derived_files_rebuild_result_in=*/
             DerivedFilesRebuildResult(
-                /*needs_document_store_derived_files_rebuild=*/true,
-                /*needs_schema_store_derived_files_rebuild=*/true,
-                /*needs_term_index_rebuild=*/true,
-                /*needs_integer_index_rebuild=*/true,
-                /*needs_qualified_id_join_index_rebuild=*/true))));
+                /*needs_document_store_derived_files_rebuild_in=*/true,
+                /*needs_schema_store_derived_files_rebuild_in=*/true,
+                /*needs_term_index_rebuild_in=*/true,
+                /*needs_integer_index_rebuild_in=*/true,
+                /*needs_qualified_id_join_index_rebuild_in=*/true,
+                /*needs_embedding_index_rebuild_in=*/true)),
+
+        // - Existing version 5, max_version 5
+        // - Existing enabled features = {}
+        // - Current version = 5
+        // - Current enabled features = {FEATURE_HAS_PROPERTY_OPERATOR}
+        //
+        // - Result: rebuild term index
+        VersionUtilDerivedFilesRebuildTestParam(
+            /*existing_version_in=*/5, /*max_version_in=*/5,
+            /*existing_enabled_features_in=*/{}, /*curr_version_in=*/5,
+            /*curr_enabled_features_in=*/
+            {IcingSearchEngineFeatureInfoProto::FEATURE_HAS_PROPERTY_OPERATOR},
+            /*expected_derived_files_rebuild_result_in=*/
+            DerivedFilesRebuildResult(
+                /*needs_document_store_derived_files_rebuild_in=*/false,
+                /*needs_schema_store_derived_files_rebuild_in=*/false,
+                /*needs_term_index_rebuild_in=*/true,
+                /*needs_integer_index_rebuild_in=*/false,
+                /*needs_qualified_id_join_index_rebuild_in=*/false,
+                /*needs_embedding_index_rebuild_in=*/false)),
+
+        // - Existing version 5, max_version 5
+        // - Existing enabled features = {}
+        // - Current version = 5
+        // - Current enabled features = {FEATURE_SCHEMA_DATABASE}
+        //
+        // - Result: no rebuild
+        VersionUtilDerivedFilesRebuildTestParam(
+            /*existing_version_in=*/5, /*max_version_in=*/5,
+            /*existing_enabled_features_in=*/{},
+            /*curr_version_in=*/5, /*curr_enabled_features_in=*/
+            {IcingSearchEngineFeatureInfoProto::FEATURE_SCHEMA_DATABASE},
+            /*expected_derived_files_rebuild_result_in=*/
+            DerivedFilesRebuildResult(
+                /*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=*/false,
+                /*needs_embedding_index_rebuild_in=*/false)),
+
+        // - Existing version 5, max_version 5
+        // - Existing enabled features = {}
+        // - Current version = 5
+        // - Current enabled features =
+        //   {FEATURE_QUALIFIED_ID_JOIN_INDEX_V3_AND_DELETE_PROPAGATE_FROM}
+        //
+        // - 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_QUALIFIED_ID_JOIN_INDEX_V3_AND_DELETE_PROPAGATE_FROM},
+            /*expected_derived_files_rebuild_result_in=*/
+            DerivedFilesRebuildResult(
+                /*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_QUALIFIED_ID_JOIN_INDEX_V3_AND_DELETE_PROPAGATE_FROM}
+        // - 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_QUALIFIED_ID_JOIN_INDEX_V3_AND_DELETE_PROPAGATE_FROM},
+            /*curr_version_in=*/5, /*curr_enabled_features_in=*/{},
+            /*expected_derived_files_rebuild_result_in=*/
+            DerivedFilesRebuildResult(
+                /*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) {
   EXPECT_THAT(
@@ -792,6 +1104,10 @@
   // (2 -> 0), 0 -> 1
   EXPECT_THAT(ShouldRebuildDerivedFiles(VersionInfo(0, 2), /*curr_version=*/1),
               IsTrue());
+
+  // (5 -> 0), 0 -> 5
+  EXPECT_THAT(ShouldRebuildDerivedFiles(VersionInfo(0, 5), /*curr_version=*/5),
+              IsTrue());
 }
 
 TEST(VersionUtilTest, ShouldRebuildDerivedFilesRollBack) {
@@ -806,6 +1122,10 @@
   // (3 -> 2), 2 -> 1
   EXPECT_THAT(ShouldRebuildDerivedFiles(VersionInfo(2, 3), /*curr_version=*/1),
               IsTrue());
+
+  // (5 -> 4), 4 -> 3
+  EXPECT_THAT(ShouldRebuildDerivedFiles(VersionInfo(4, 5), /*curr_version=*/3),
+              IsTrue());
 }
 
 TEST(VersionUtilTest, ShouldRebuildDerivedFilesRollForward) {
@@ -820,6 +1140,10 @@
   // (3 -> 1), 1 -> 2
   EXPECT_THAT(ShouldRebuildDerivedFiles(VersionInfo(1, 3), /*curr_version=*/2),
               IsTrue());
+
+  // (5 -> 4), 4 -> 5
+  EXPECT_THAT(ShouldRebuildDerivedFiles(VersionInfo(4, 5), /*curr_version=*/5),
+              IsTrue());
 }
 
 TEST(VersionUtilTest, ShouldRebuildDerivedFilesCompatible) {
@@ -863,17 +1187,39 @@
   EXPECT_THAT(ShouldRebuildDerivedFiles(VersionInfo(kVersionOne, kVersionOne),
                                         /*curr_version=*/kVersionFour),
               IsFalse());
+
+  // kVersionFour -> kVersionFive.
+  EXPECT_THAT(ShouldRebuildDerivedFiles(VersionInfo(kVersionFour, kVersionFour),
+                                        /*curr_version=*/kVersionFive),
+              IsFalse());
+
+  // kVersionThree -> kVersionFive.
+  EXPECT_THAT(
+      ShouldRebuildDerivedFiles(VersionInfo(kVersionThree, kVersionThree),
+                                /*curr_version=*/kVersionFive),
+      IsFalse());
+
+  // kVersionTwo -> kVersionFour
+  EXPECT_THAT(ShouldRebuildDerivedFiles(VersionInfo(kVersionTwo, kVersionTwo),
+                                        /*curr_version=*/kVersionFive),
+              IsFalse());
+
+  // kVersionOne -> kVersionFour.
+  EXPECT_THAT(ShouldRebuildDerivedFiles(VersionInfo(kVersionOne, kVersionOne),
+                                        /*curr_version=*/kVersionFive),
+              IsFalse());
 }
 
 TEST(VersionUtilTest, GetFeatureDerivedFilesRebuildResult_unknown) {
   EXPECT_THAT(GetFeatureDerivedFilesRebuildResult(
                   IcingSearchEngineFeatureInfoProto::UNKNOWN),
               Eq(DerivedFilesRebuildResult(
-                  /*needs_document_store_derived_files_rebuild=*/true,
-                  /*needs_schema_store_derived_files_rebuild=*/true,
-                  /*needs_term_index_rebuild=*/true,
-                  /*needs_integer_index_rebuild=*/true,
-                  /*needs_qualified_id_join_index_rebuild=*/true)));
+                  /*needs_document_store_derived_files_rebuild_in=*/true,
+                  /*needs_schema_store_derived_files_rebuild_in=*/true,
+                  /*needs_term_index_rebuild_in=*/true,
+                  /*needs_integer_index_rebuild_in=*/true,
+                  /*needs_qualified_id_join_index_rebuild_in=*/true,
+                  /*needs_embedding_index_rebuild_in=*/true)));
 }
 
 TEST(VersionUtilTest,
@@ -882,11 +1228,112 @@
       GetFeatureDerivedFilesRebuildResult(
           IcingSearchEngineFeatureInfoProto::FEATURE_HAS_PROPERTY_OPERATOR),
       Eq(DerivedFilesRebuildResult(
-          /*needs_document_store_derived_files_rebuild=*/false,
-          /*needs_schema_store_derived_files_rebuild=*/false,
-          /*needs_term_index_rebuild=*/true,
-          /*needs_integer_index_rebuild=*/false,
-          /*needs_qualified_id_join_index_rebuild=*/false)));
+          /*needs_document_store_derived_files_rebuild_in=*/false,
+          /*needs_schema_store_derived_files_rebuild_in=*/false,
+          /*needs_term_index_rebuild_in=*/true,
+          /*needs_integer_index_rebuild_in=*/false,
+          /*needs_qualified_id_join_index_rebuild_in=*/false,
+          /*needs_embedding_index_rebuild_in=*/false)));
+}
+
+TEST(VersionUtilTest,
+     GetFeatureDerivedFilesRebuildResult_featureEmbeddingIndex) {
+  EXPECT_THAT(GetFeatureDerivedFilesRebuildResult(
+                  IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_INDEX),
+              Eq(DerivedFilesRebuildResult(
+                  /*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=*/false,
+                  /*needs_embedding_index_rebuild_in=*/true)));
+}
+
+TEST(VersionUtilTest,
+     GetFeatureDerivedFilesRebuildResult_featureEmbeddingQuantization) {
+  EXPECT_THAT(
+      GetFeatureDerivedFilesRebuildResult(
+          IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_QUANTIZATION),
+      Eq(DerivedFilesRebuildResult(
+          /*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=*/false,
+          /*needs_embedding_index_rebuild_in=*/true)));
+}
+
+TEST(VersionUtilTest,
+     GetFeatureDerivedFilesRebuildResult_featureSchemaDatabase) {
+  EXPECT_THAT(GetFeatureDerivedFilesRebuildResult(
+                  IcingSearchEngineFeatureInfoProto::FEATURE_SCHEMA_DATABASE),
+              Eq(DerivedFilesRebuildResult(
+                  /*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=*/false,
+                  /*needs_embedding_index_rebuild_in=*/false)));
+}
+
+TEST(
+    VersionUtilTest,
+    GetFeatureDerivedFilesRebuildResult_featureQualifiedIdJoinIndexV3AndDeletePropagateFrom) {
+  EXPECT_THAT(
+      GetFeatureDerivedFilesRebuildResult(
+          IcingSearchEngineFeatureInfoProto::
+              FEATURE_QUALIFIED_ID_JOIN_INDEX_V3_AND_DELETE_PROPAGATE_FROM),
+      Eq(DerivedFilesRebuildResult(
+          /*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.
+  IcingSearchEngineVersionProto previous_version_proto;
+  previous_version_proto.set_version(kSchemaDatabaseVersion - 1);
+  previous_version_proto.set_max_version(kSchemaDatabaseVersion - 1);
+  previous_version_proto.add_enabled_features()->set_feature_type(
+      IcingSearchEngineFeatureInfoProto::FEATURE_SCHEMA_DATABASE);
+  EXPECT_TRUE(SchemaDatabaseMigrationRequired(previous_version_proto));
+
+  // Migration is required if the schema database feature was not enabled in the
+  // previous version.
+  previous_version_proto.set_version(kSchemaDatabaseVersion);
+  previous_version_proto.set_max_version(kSchemaDatabaseVersion);
+  previous_version_proto.mutable_enabled_features()->Clear();
+  // Add a feature that is not the schema database feature.
+  previous_version_proto.add_enabled_features()->set_feature_type(
+      IcingSearchEngineFeatureInfoProto::FEATURE_HAS_PROPERTY_OPERATOR);
+  EXPECT_TRUE(SchemaDatabaseMigrationRequired(previous_version_proto));
+
+  previous_version_proto.set_version(kSchemaDatabaseVersion + 1);
+  previous_version_proto.set_max_version(kSchemaDatabaseVersion + 1);
+  previous_version_proto.mutable_enabled_features()->Clear();
+  EXPECT_TRUE(SchemaDatabaseMigrationRequired(previous_version_proto));
+}
+
+TEST(VersionUtilTest, SchemaDatabaseMigrationNotRequired) {
+  // Migration is not required if previous version is >= the version at which
+  // the schema database is introduced and the schema database feature was
+  // enabled in the previous version.
+  IcingSearchEngineVersionProto previous_version_proto;
+  previous_version_proto.set_version(kSchemaDatabaseVersion);
+  previous_version_proto.set_max_version(kSchemaDatabaseVersion);
+  previous_version_proto.add_enabled_features()->set_feature_type(
+      IcingSearchEngineFeatureInfoProto::FEATURE_SCHEMA_DATABASE);
+  EXPECT_FALSE(SchemaDatabaseMigrationRequired(previous_version_proto));
+
+  previous_version_proto.set_version(kSchemaDatabaseVersion + 1);
+  previous_version_proto.set_max_version(kSchemaDatabaseVersion + 1);
+  previous_version_proto.add_enabled_features()->set_feature_type(
+      IcingSearchEngineFeatureInfoProto::FEATURE_SCHEMA_DATABASE);
+  EXPECT_FALSE(SchemaDatabaseMigrationRequired(previous_version_proto));
 }
 
 class VersionUtilFeatureProtoTest
@@ -913,13 +1360,21 @@
               Eq(rebuild_result.needs_integer_index_rebuild));
   EXPECT_THAT(feature_info.needs_qualified_id_join_index_rebuild(),
               Eq(rebuild_result.needs_qualified_id_join_index_rebuild));
+  EXPECT_THAT(feature_info.needs_embedding_index_rebuild(),
+              Eq(rebuild_result.needs_embedding_index_rebuild));
 }
 
 INSTANTIATE_TEST_SUITE_P(
     VersionUtilFeatureProtoTest, VersionUtilFeatureProtoTest,
     testing::Values(
         IcingSearchEngineFeatureInfoProto::UNKNOWN,
-        IcingSearchEngineFeatureInfoProto::FEATURE_HAS_PROPERTY_OPERATOR));
+        IcingSearchEngineFeatureInfoProto::FEATURE_HAS_PROPERTY_OPERATOR,
+        IcingSearchEngineFeatureInfoProto::FEATURE_SCORABLE_PROPERTIES,
+        IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_INDEX,
+        IcingSearchEngineFeatureInfoProto::FEATURE_EMBEDDING_QUANTIZATION,
+        IcingSearchEngineFeatureInfoProto::FEATURE_SCHEMA_DATABASE,
+        IcingSearchEngineFeatureInfoProto::
+            FEATURE_QUALIFIED_ID_JOIN_INDEX_V3_AND_DELETE_PROPAGATE_FROM));
 
 }  // namespace
 
diff --git a/icing/icing-search-engine.cc b/icing/icing-search-engine.cc
index 769e5de..8cfcef9 100644
--- a/icing/icing-search-engine.cc
+++ b/icing/icing-search-engine.cc
@@ -32,6 +32,7 @@
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/absl_ports/mutex.h"
 #include "icing/absl_ports/str_cat.h"
+#include "icing/feature-flags.h"
 #include "icing/file/destructible-file.h"
 #include "icing/file/file-backed-proto.h"
 #include "icing/file/filesystem.h"
@@ -50,8 +51,8 @@
 #include "icing/jni/jni-cache.h"
 #include "icing/join/join-children-fetcher.h"
 #include "icing/join/join-processor.h"
-#include "icing/join/qualified-id-join-index-impl-v1.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/legacy/index/icing-filesystem.h"
@@ -101,6 +102,7 @@
 #include "icing/util/status-macros.h"
 #include "icing/util/tokenized-document.h"
 #include "unicode/uloc.h"
+#include <google/protobuf/repeated_field.h>
 
 namespace icing {
 namespace lib {
@@ -238,25 +240,59 @@
   return libtextclassifier3::Status::OK;
 }
 
-bool IsV2QualifiedIdJoinIndexEnabled(const IcingSearchEngineOptions& options) {
-  return true;
+libtextclassifier3::Status ValidateScoringSpec(
+    const ScoringSpecProto& scoring_spec) {
+  std::unordered_set<std::string> alias_schema_types;
+  for (const SchemaTypeAliasMapProto& alias_map_proto :
+       scoring_spec.schema_type_alias_map_protos()) {
+    if (alias_map_proto.alias_schema_type().empty()) {
+      return absl_ports::InvalidArgumentError(
+          "SchemaTypeAliasMapProto contains alias_schema_type with empty "
+          "string");
+    }
+    if (alias_map_proto.schema_types().empty()) {
+      return absl_ports::InvalidArgumentError(
+          absl_ports::StrCat("SchemaTypeAliasMapProto contains empty "
+                             "schema_types for alias_schema_type: ",
+                             alias_map_proto.alias_schema_type()));
+    }
+    if (alias_schema_types.find(alias_map_proto.alias_schema_type()) !=
+        alias_schema_types.end()) {
+      return absl_ports::InvalidArgumentError(
+          absl_ports::StrCat("SchemaTypeAliasMapProto contains multiple "
+                             "entries with the same alias_schema_type: ",
+                             alias_map_proto.alias_schema_type()));
+    }
+    alias_schema_types.insert(alias_map_proto.alias_schema_type());
+  }
+  return libtextclassifier3::Status::OK;
+}
+
+ScoringSpecProto CopyParentSchemaTypeAliasMapToChild(
+    const ScoringSpecProto& parent_scoring_spec,
+    const ScoringSpecProto& child_scoring_spec) {
+  ScoringSpecProto new_child_scoring_spec = std::move(child_scoring_spec);
+  for (const SchemaTypeAliasMapProto& alias_map_proto :
+       parent_scoring_spec.schema_type_alias_map_protos()) {
+    *new_child_scoring_spec.add_schema_type_alias_map_protos() =
+        alias_map_proto;
+  }
+  return new_child_scoring_spec;
 }
 
 libtextclassifier3::StatusOr<std::unique_ptr<QualifiedIdJoinIndex>>
 CreateQualifiedIdJoinIndex(const Filesystem& filesystem,
                            std::string qualified_id_join_index_dir,
-                           const IcingSearchEngineOptions& options) {
-  if (IsV2QualifiedIdJoinIndexEnabled(options)) {
+                           const IcingSearchEngineOptions& options,
+                           const FeatureFlags& feature_flags) {
+  if (options.enable_qualified_id_join_index_v3_and_delete_propagate_from()) {
+    return QualifiedIdJoinIndexImplV3::Create(
+        filesystem, std::move(qualified_id_join_index_dir), feature_flags);
+  } else {
     // V2
     return QualifiedIdJoinIndexImplV2::Create(
         filesystem, std::move(qualified_id_join_index_dir),
         options.pre_mapping_fbv());
-  } else {
-    // V1
-    // TODO(b/275121148): deprecate this part after rollout v2.
-    return QualifiedIdJoinIndexImplV1::Create(
-        filesystem, std::move(qualified_id_join_index_dir),
-        options.pre_mapping_fbv(), options.use_persistent_hash_map());
   }
 }
 
@@ -481,6 +517,9 @@
     std::unique_ptr<const IcingFilesystem> icing_filesystem,
     std::unique_ptr<Clock> clock, std::unique_ptr<const JniCache> jni_cache)
     : options_(std::move(options)),
+      feature_flags_(options_.enable_scorable_properties(),
+                     options_.enable_embedding_quantization(),
+                     options_.enable_repeated_field_joins()),
       filesystem_(std::move(filesystem)),
       icing_filesystem_(std::move(icing_filesystem)),
       clock_(std::move(clock)),
@@ -659,13 +698,15 @@
       std::max(stored_version_info.max_version, version_util::kVersion));
   version_util::AddEnabledFeatures(options_, &current_version_proto);
 
-  // Step 1: If versions are incompatible, migrate schema according to the
-  // version state change.
-  if (version_state_change != version_util::StateChange::kCompatible) {
-    ICING_RETURN_IF_ERROR(SchemaStore::MigrateSchema(
-        filesystem_.get(), MakeSchemaDirectoryPath(options_.base_dir()),
-        version_state_change, version_util::kVersion));
-  }
+  // Step 1: Perform schema migration if needed. This is a no-op if the schema
+  // is fully compatible with the current version.
+  bool perform_schema_database_migration =
+      version_util::SchemaDatabaseMigrationRequired(stored_version_proto) &&
+      options_.enable_schema_database();
+  ICING_RETURN_IF_ERROR(SchemaStore::MigrateSchema(
+      filesystem_.get(), MakeSchemaDirectoryPath(options_.base_dir()),
+      version_state_change, version_util::kVersion,
+      perform_schema_database_migration));
 
   // Step 2: Discard derived files that need to be rebuilt
   version_util::DerivedFilesRebuildResult required_derived_files_rebuild =
@@ -727,7 +768,8 @@
     }
     if (options_.enable_blob_store()) {
       ICING_RETURN_IF_ERROR(
-          InitializeBlobStore(options_.orphan_blob_time_to_live_ms()));
+          InitializeBlobStore(options_.orphan_blob_time_to_live_ms(),
+                              options_.blob_store_compression_level()));
     }
     ICING_ASSIGN_OR_RETURN(
         bool document_store_derived_files_regenerated,
@@ -748,7 +790,8 @@
     // unused.
     if (options_.enable_blob_store()) {
       ICING_RETURN_IF_ERROR(
-          InitializeBlobStore(options_.orphan_blob_time_to_live_ms()));
+          InitializeBlobStore(options_.orphan_blob_time_to_live_ms(),
+                              options_.blob_store_compression_level()));
     }
     ICING_RETURN_IF_ERROR(InitializeDocumentStore(
         /*force_recovery_and_revalidate_documents=*/true, initialize_stats));
@@ -786,8 +829,9 @@
         *filesystem_, qualified_id_join_index_dir));
     ICING_ASSIGN_OR_RETURN(
         qualified_id_join_index_,
-        CreateQualifiedIdJoinIndex(
-            *filesystem_, std::move(qualified_id_join_index_dir), options_));
+        CreateQualifiedIdJoinIndex(*filesystem_,
+                                   std::move(qualified_id_join_index_dir),
+                                   options_, feature_flags_));
 
     // Discard embedding index directory and instantiate a new one.
     std::string embedding_index_dir =
@@ -796,7 +840,8 @@
         EmbeddingIndex::Discard(*filesystem_, embedding_index_dir));
     ICING_ASSIGN_OR_RETURN(
         embedding_index_,
-        EmbeddingIndex::Create(filesystem_.get(), embedding_index_dir));
+        EmbeddingIndex::Create(filesystem_.get(), embedding_index_dir,
+                               clock_.get(), &feature_flags_));
 
     std::unique_ptr<Timer> restore_timer = clock_->GetNewTimer();
     IndexRestorationResult restore_result = RestoreIndexIfNeeded();
@@ -825,7 +870,8 @@
   } else if (version_state_change != version_util::StateChange::kCompatible) {
     if (options_.enable_blob_store()) {
       ICING_RETURN_IF_ERROR(
-          InitializeBlobStore(options_.orphan_blob_time_to_live_ms()));
+          InitializeBlobStore(options_.orphan_blob_time_to_live_ms(),
+                              options_.blob_store_compression_level()));
     }
     ICING_ASSIGN_OR_RETURN(bool document_store_derived_files_regenerated,
                            InitializeDocumentStore(
@@ -852,7 +898,8 @@
   } else {
     if (options_.enable_blob_store()) {
       ICING_RETURN_IF_ERROR(
-          InitializeBlobStore(options_.orphan_blob_time_to_live_ms()));
+          InitializeBlobStore(options_.orphan_blob_time_to_live_ms(),
+                              options_.blob_store_compression_level()));
     }
     ICING_ASSIGN_OR_RETURN(
         bool document_store_derived_files_regenerated,
@@ -889,7 +936,10 @@
       initialize_stats->set_qualified_id_join_index_restoration_cause(
           InitializeStatsProto::FEATURE_FLAG_CHANGED);
     }
-    // TODO(b/326656531): Update version-util to consider embedding index.
+    if (required_derived_files_rebuild.needs_embedding_index_rebuild) {
+      initialize_stats->set_embedding_index_restoration_cause(
+          InitializeStatsProto::FEATURE_FLAG_CHANGED);
+    }
   }
 
   if (status.ok()) {
@@ -914,8 +964,10 @@
         absl_ports::StrCat("Could not create directory: ", schema_store_dir));
   }
   ICING_ASSIGN_OR_RETURN(
-      schema_store_, SchemaStore::Create(filesystem_.get(), schema_store_dir,
-                                         clock_.get(), initialize_stats));
+      schema_store_,
+      SchemaStore::Create(filesystem_.get(), schema_store_dir, clock_.get(),
+                          &feature_flags_, options_.enable_schema_database(),
+                          initialize_stats));
 
   return libtextclassifier3::Status::OK;
 }
@@ -936,8 +988,7 @@
       DocumentStore::CreateResult create_result,
       DocumentStore::Create(
           filesystem_.get(), document_dir, clock_.get(), schema_store_.get(),
-          force_recovery_and_revalidate_documents,
-          /*document_store_namespace_id_fingerprint=*/true,
+          &feature_flags_, force_recovery_and_revalidate_documents,
           /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
           options_.compression_level(), initialize_stats));
   document_store_ = std::move(create_result.document_store);
@@ -945,7 +996,7 @@
 }
 
 libtextclassifier3::Status IcingSearchEngine::InitializeBlobStore(
-    int32_t orphan_blob_time_to_live_ms) {
+    int32_t orphan_blob_time_to_live_ms, int32_t blob_store_compression_level) {
   std::string blob_dir = MakeBlobDirectoryPath(options_.base_dir());
   // Make sure the sub-directory exists
   if (!filesystem_->CreateDirectoryRecursively(blob_dir.c_str())) {
@@ -956,7 +1007,8 @@
   ICING_ASSIGN_OR_RETURN(
       auto blob_store_or,
       BlobStore::Create(filesystem_.get(), blob_dir, clock_.get(),
-                        orphan_blob_time_to_live_ms));
+                        orphan_blob_time_to_live_ms,
+                        blob_store_compression_level));
   blob_store_ = std::make_unique<BlobStore>(std::move(blob_store_or));
   return libtextclassifier3::Status::OK;
 }
@@ -1035,7 +1087,7 @@
       MakeQualifiedIdJoinIndexWorkingPath(options_.base_dir());
   InitializeStatsProto::RecoveryCause qualified_id_join_index_recovery_cause;
   if (document_store_derived_files_regenerated &&
-      IsV2QualifiedIdJoinIndexEnabled(options_)) {
+      !options_.enable_qualified_id_join_index_v3_and_delete_propagate_from()) {
     // 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.
@@ -1044,14 +1096,15 @@
 
     ICING_ASSIGN_OR_RETURN(
         qualified_id_join_index_,
-        CreateQualifiedIdJoinIndex(
-            *filesystem_, std::move(qualified_id_join_index_dir), options_));
+        CreateQualifiedIdJoinIndex(*filesystem_,
+                                   std::move(qualified_id_join_index_dir),
+                                   options_, feature_flags_));
 
     qualified_id_join_index_recovery_cause =
         InitializeStatsProto::DEPENDENCIES_CHANGED;
   } else {
     auto qualified_id_join_index_or = CreateQualifiedIdJoinIndex(
-        *filesystem_, qualified_id_join_index_dir, options_);
+        *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));
@@ -1061,8 +1114,9 @@
       // 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_));
+          CreateQualifiedIdJoinIndex(*filesystem_,
+                                     std::move(qualified_id_join_index_dir),
+                                     options_, feature_flags_));
     } else {
       // Qualified id join index was created fine.
       qualified_id_join_index_ =
@@ -1078,8 +1132,8 @@
   const std::string embedding_dir =
       MakeEmbeddingIndexWorkingPath(options_.base_dir());
   InitializeStatsProto::RecoveryCause embedding_index_recovery_cause;
-  auto embedding_index_or =
-      EmbeddingIndex::Create(filesystem_.get(), embedding_dir);
+  auto embedding_index_or = EmbeddingIndex::Create(
+      filesystem_.get(), embedding_dir, clock_.get(), &feature_flags_);
   if (!embedding_index_or.ok()) {
     ICING_RETURN_IF_ERROR(EmbeddingIndex::Discard(*filesystem_, embedding_dir));
 
@@ -1088,7 +1142,8 @@
     // Try recreating it from scratch and re-indexing everything.
     ICING_ASSIGN_OR_RETURN(
         embedding_index_,
-        EmbeddingIndex::Create(filesystem_.get(), embedding_dir));
+        EmbeddingIndex::Create(filesystem_.get(), embedding_dir, clock_.get(),
+                               &feature_flags_));
   } else {
     // Embedding index was created fine.
     embedding_index_ = std::move(embedding_index_or).ValueOrDie();
@@ -1267,6 +1322,19 @@
       }
     }
 
+    if (feature_flags_.enable_scorable_properties()) {
+      if (!set_schema_result.schema_types_scorable_property_inconsistent_by_id
+               .empty()) {
+        status = document_store_->RegenerateScorablePropertyCache(
+            set_schema_result
+                .schema_types_scorable_property_inconsistent_by_id);
+        if (!status.ok()) {
+          TransformStatus(status, result_status);
+          return result_proto;
+        }
+      }
+    }
+
     result_status->set_code(StatusProto::OK);
   } else {
     result_status->set_code(StatusProto::FAILED_PRECONDITION);
@@ -1298,6 +1366,29 @@
   return result_proto;
 }
 
+GetSchemaResultProto IcingSearchEngine::GetSchema(std::string_view database) {
+  GetSchemaResultProto 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!");
+    return result_proto;
+  }
+
+  libtextclassifier3::StatusOr<SchemaProto> schema =
+      schema_store_->GetSchema(std::string(database));
+  if (!schema.ok()) {
+    TransformStatus(schema.status(), result_status);
+    return result_proto;
+  }
+
+  result_status->set_code(StatusProto::OK);
+  *result_proto.mutable_schema() = std::move(schema).ValueOrDie();
+  return result_proto;
+}
+
 GetSchemaTypeResultProto IcingSearchEngine::GetSchemaType(
     std::string_view schema_type) {
   GetSchemaTypeResultProto result_proto;
@@ -1362,8 +1453,10 @@
     TransformStatus(put_result_or.status(), result_status);
     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);
+  result_proto.set_was_replacement(
+      put_result_or.ValueOrDie().was_replacement());
 
   auto data_indexing_handlers_or = CreateDataIndexingHandlers();
   if (!data_indexing_handlers_or.ok()) {
@@ -1374,7 +1467,7 @@
       std::move(data_indexing_handlers_or).ValueOrDie(), clock_.get());
 
   auto index_status = index_processor.IndexDocument(
-      tokenized_document, document_id, put_document_stats);
+      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)) {
@@ -1515,27 +1608,63 @@
   DeleteStatsProto* delete_stats = result_proto.mutable_delete_stats();
   delete_stats->set_delete_type(DeleteStatsProto::DeleteType::SINGLE);
 
+  libtextclassifier3::Status status;
+  libtextclassifier3::Status propagate_delete_status;
+  int num_documents_deleted = 0;
   std::unique_ptr<Timer> delete_timer = clock_->GetNewTimer();
-  // TODO(b/216487496): Implement a more robust version of TC_RETURN_IF_ERROR
-  // that can support error logging.
-  int64_t current_time_ms = clock_->GetSystemTimeMilliseconds();
-  libtextclassifier3::Status status =
-      document_store_->Delete(name_space, uri, current_time_ms);
+
+  libtextclassifier3::StatusOr<DocumentId> document_id_or =
+      document_store_->GetDocumentId(name_space, uri);
+  if (!document_id_or.ok()) {
+    status = std::move(document_id_or).status();
+  } else {
+    DocumentId document_id = document_id_or.ValueOrDie();
+
+    // TODO(b/216487496): Implement a more robust version of TC_RETURN_IF_ERROR
+    // that can support error logging.
+    int64_t current_time_ms = clock_->GetSystemTimeMilliseconds();
+    status = document_store_->Delete(document_id, current_time_ms);
+    if (status.ok()) {
+      ++num_documents_deleted;
+    }
+
+    // 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);
+    if (propagated_child_docs_deleted_or.ok()) {
+      num_documents_deleted += propagated_child_docs_deleted_or.ValueOrDie();
+    } else {
+      propagate_delete_status =
+          std::move(propagated_child_docs_deleted_or).status();
+    }
+  }
+  delete_stats->set_num_documents_deleted(num_documents_deleted);
+  delete_stats->set_latency_ms(delete_timer->GetElapsedMilliseconds());
+
   if (!status.ok()) {
     LogSeverity::Code severity = ERROR;
     if (absl_ports::IsNotFound(status)) {
       severity = DBG;
     }
     ICING_LOG(severity) << status.error_message()
-                        << "Failed to delete Document. namespace: "
+                        << ". Failed to delete Document. namespace: "
                         << name_space << ", uri: " << uri;
     TransformStatus(status, result_status);
     return result_proto;
   }
 
+  if (!propagate_delete_status.ok()) {
+    ICING_LOG(ERROR) << propagate_delete_status.error_message()
+                     << ". Failed to propagate delete for document. namespace: "
+                     << name_space << ", uri: " << uri;
+    TransformStatus(propagate_delete_status, result_status);
+    return result_proto;
+  }
+
   result_status->set_code(StatusProto::OK);
-  delete_stats->set_latency_ms(delete_timer->GetElapsedMilliseconds());
-  delete_stats->set_num_documents_deleted(1);
   return result_proto;
 }
 
@@ -1645,7 +1774,8 @@
   auto query_processor_or = QueryProcessor::Create(
       index_.get(), integer_index_.get(), embedding_index_.get(),
       language_segmenter_.get(), normalizer_.get(), document_store_.get(),
-      schema_store_.get(), clock_.get());
+      schema_store_.get(), /*join_children_fetcher=*/nullptr, clock_.get(),
+      &feature_flags_);
   if (!query_processor_or.ok()) {
     TransformStatus(query_processor_or.status(), result_status);
     delete_stats->set_parse_query_latency_ms(
@@ -1722,6 +1852,44 @@
   return result_proto;
 }
 
+libtextclassifier3::StatusOr<int> IcingSearchEngine::PropagateDelete(
+    const std::unordered_set<DocumentId>& deleted_document_ids,
+    int64_t current_time_ms) {
+  int propagated_child_docs_deleted = 0;
+
+  if (!options_.enable_qualified_id_join_index_v3_and_delete_propagate_from() ||
+      qualified_id_join_index_->version() !=
+          QualifiedIdJoinIndex::Version::kV3) {
+    // No-op if delete propagation is disabled or the join index is not v3.
+    return propagated_child_docs_deleted;
+  }
+
+  // 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;
+}
+
 PersistToDiskResultProto IcingSearchEngine::PersistToDisk(
     PersistType::Code persist_type) {
   ICING_VLOG(1) << "Persisting data to disk";
@@ -1863,7 +2031,8 @@
     }
 
     libtextclassifier3::Status embedding_index_optimize_status =
-        embedding_index_->Optimize(optimize_result.document_id_old_to_new,
+        embedding_index_->Optimize(document_store_.get(), schema_store_.get(),
+                                   optimize_result.document_id_old_to_new,
                                    document_store_->last_added_document_id());
     if (!embedding_index_optimize_status.ok()) {
       ICING_LOG(WARNING) << "Failed to optimize embedding index. Error: "
@@ -2039,6 +2208,24 @@
       schema_store_->GetStorageInfo();
   *result.mutable_storage_info()->mutable_index_storage_info() =
       index_->GetStorageInfo();
+  if (blob_store_ != nullptr) {
+    auto namespace_blob_storage_infos_or = blob_store_->GetStorageInfo();
+    if (!namespace_blob_storage_infos_or.ok()) {
+      result.mutable_status()->set_code(StatusProto::INTERNAL);
+      result.mutable_status()->set_message(
+          namespace_blob_storage_infos_or.status().error_message());
+      return result;
+    }
+    std::vector<NamespaceBlobStorageInfoProto> namespace_blob_storage_infos =
+        std::move(namespace_blob_storage_infos_or).ValueOrDie();
+
+    for (NamespaceBlobStorageInfoProto& namespace_blob_storage_info :
+         namespace_blob_storage_infos) {
+      *result.mutable_storage_info()
+           ->mutable_namespace_blob_storage_info()
+           ->Add() = std::move(namespace_blob_storage_info);
+    }
+  }
   // TODO(b/259744228): add stats for integer index
   result.mutable_status()->set_code(StatusProto::OK);
   return result;
@@ -2090,8 +2277,8 @@
     PersistType::Code persist_type) {
   if (blob_store_ != nullptr) {
     // For all valid PersistTypes, we persist the ground truth. The ground truth
-    // in the schema_store is always persisted immediately after changes are
-    // applied. So there is nothing to do if persist_type is LITE.
+    // 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());
   }
   ICING_RETURN_IF_ERROR(document_store_->PersistToDisk(persist_type));
@@ -2201,6 +2388,11 @@
     TransformStatus(status, result_status);
     return result_proto;
   }
+  status = ValidateScoringSpec(scoring_spec);
+  if (!status.ok()) {
+    TransformStatus(status, result_status);
+    return result_proto;
+  }
 
   const JoinSpecProto& join_spec = search_spec.join_spec();
   std::unique_ptr<JoinChildrenFetcher> join_children_fetcher;
@@ -2212,10 +2404,23 @@
     QueryStatsProto::SearchStats* child_search_stats =
         query_stats->mutable_child_search_stats();
 
+    // Build a child scoring spec by copying the parent schema type alias map.
+    // Note that this function will transfer the ownership of data from
+    // search_spec.join_spec().nested_spec().scoring_spec() to the
+    // child_scoring_spec.
+    // Hence, that following functions should NOT access
+    // search_spec.join_spec().nested_spec().scoring_spec() after this function
+    // call, but using child_scoring_spec instead.
+    //
+    // TODO(b/379288742): Avoid making the copy of the parent schema type alias
+    // map.
+    ScoringSpecProto child_scoring_spec = CopyParentSchemaTypeAliasMapToChild(
+        scoring_spec, search_spec.join_spec().nested_spec().scoring_spec());
+
     // Process child query
+    // TODO(b/372541905): Validate the child search spec.
     QueryScoringResults nested_query_scoring_results = ProcessQueryAndScore(
-        join_spec.nested_spec().search_spec(),
-        join_spec.nested_spec().scoring_spec(),
+        join_spec.nested_spec().search_spec(), child_scoring_spec,
         join_spec.nested_spec().result_spec(),
         /*join_children_fetcher=*/nullptr, current_time_ms, child_search_stats);
     if (!nested_query_scoring_results.status.ok()) {
@@ -2226,23 +2431,21 @@
     JoinProcessor join_processor(document_store_.get(), schema_store_.get(),
                                  qualified_id_join_index_.get(),
                                  current_time_ms);
-    // Building a JoinChildrenFetcher where child documents are grouped by
-    // their joinable values.
-    libtextclassifier3::StatusOr<JoinChildrenFetcher> join_children_fetcher_or =
-        join_processor.GetChildrenFetcher(
+    // Building a JoinChildrenFetcher for looking up child documents by parent
+    // document id.
+    libtextclassifier3::StatusOr<std::unique_ptr<JoinChildrenFetcher>>
+        join_children_fetcher_or = join_processor.GetChildrenFetcher(
             search_spec.join_spec(),
             std::move(nested_query_scoring_results.scored_document_hits));
     if (!join_children_fetcher_or.ok()) {
       TransformStatus(join_children_fetcher_or.status(), result_status);
       return result_proto;
     }
-    join_children_fetcher = std::make_unique<JoinChildrenFetcher>(
-        std::move(join_children_fetcher_or).ValueOrDie());
+    join_children_fetcher = std::move(join_children_fetcher_or).ValueOrDie();
 
     // Assign child's ResultAdjustmentInfo.
     child_result_adjustment_info = std::make_unique<ResultAdjustmentInfo>(
-        join_spec.nested_spec().search_spec(),
-        join_spec.nested_spec().scoring_spec(),
+        join_spec.nested_spec().search_spec(), child_scoring_spec,
         join_spec.nested_spec().result_spec(), schema_store_.get(),
         std::move(nested_query_scoring_results.query_terms));
   }
@@ -2394,7 +2597,8 @@
   auto query_processor_or = QueryProcessor::Create(
       index_.get(), integer_index_.get(), embedding_index_.get(),
       language_segmenter_.get(), normalizer_.get(), document_store_.get(),
-      schema_store_.get(), clock_.get());
+      schema_store_.get(), join_children_fetcher, clock_.get(),
+      &feature_flags_);
   if (!query_processor_or.ok()) {
     search_stats->set_parse_query_latency_ms(
         component_timer->GetElapsedMilliseconds());
@@ -2441,7 +2645,7 @@
           scoring_spec, /*default_semantic_metric_type=*/
           search_spec.embedding_query_metric_type(), document_store_.get(),
           schema_store_.get(), current_time_ms, join_children_fetcher,
-          &query_results.embedding_query_results);
+          &query_results.embedding_query_results, &feature_flags_);
   if (!scoring_processor_or.ok()) {
     return QueryScoringResults(std::move(scoring_processor_or).status(),
                                std::move(query_results.query_terms),
@@ -2550,7 +2754,7 @@
 }
 
 BlobProto IcingSearchEngine::OpenWriteBlob(
-    PropertyProto::BlobHandleProto blob_handle) {
+    const PropertyProto::BlobHandleProto& blob_handle) {
   BlobProto blob_proto;
   StatusProto* status = blob_proto.mutable_status();
 
@@ -2568,7 +2772,8 @@
     return blob_proto;
   }
 
-  auto write_fd_or = blob_store_->OpenWrite(blob_handle);
+  libtextclassifier3::StatusOr<int> write_fd_or =
+      blob_store_->OpenWrite(blob_handle);
   if (!write_fd_or.ok()) {
     TransformStatus(write_fd_or.status(), status);
     return blob_proto;
@@ -2578,8 +2783,35 @@
   return blob_proto;
 }
 
+BlobProto IcingSearchEngine::RemoveBlob(
+    const PropertyProto::BlobHandleProto& blob_handle) {
+  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("Remove blob is not supported in this Icing instance!");
+    return blob_proto;
+  }
+
+  if (!initialized_) {
+    status->set_code(StatusProto::FAILED_PRECONDITION);
+    status->set_message("IcingSearchEngine has not been initialized!");
+    return blob_proto;
+  }
+
+  auto remove_result = blob_store_->RemoveBlob(blob_handle);
+  if (!remove_result.ok()) {
+    TransformStatus(remove_result, status);
+    return blob_proto;
+  }
+  status->set_code(StatusProto::OK);
+  return blob_proto;
+}
+
 BlobProto IcingSearchEngine::OpenReadBlob(
-    PropertyProto::BlobHandleProto blob_handle) {
+    const PropertyProto::BlobHandleProto& blob_handle) {
   BlobProto blob_proto;
   StatusProto* status = blob_proto.mutable_status();
   absl_ports::shared_lock l(&mutex_);
@@ -2608,7 +2840,7 @@
 }
 
 BlobProto IcingSearchEngine::CommitBlob(
-    PropertyProto::BlobHandleProto blob_handle) {
+    const PropertyProto::BlobHandleProto& blob_handle) {
   BlobProto blob_proto;
   StatusProto* status = blob_proto.mutable_status();
   absl_ports::unique_lock l(&mutex_);
@@ -2692,8 +2924,8 @@
     // system in the broken state for future operations.
     auto create_result_or = DocumentStore::Create(
         filesystem_.get(), current_document_dir, clock_.get(),
-        schema_store_.get(), /*force_recovery_and_revalidate_documents=*/false,
-        /*document_store_namespace_id_fingerprint=*/true,
+        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);
     // TODO(b/144458732): Implement a more robust version of
@@ -2720,8 +2952,8 @@
   // Recreates the doc store instance
   auto create_result_or = DocumentStore::Create(
       filesystem_.get(), current_document_dir, clock_.get(),
-      schema_store_.get(), /*force_recovery_and_revalidate_documents=*/false,
-      /*document_store_namespace_id_fingerprint=*/true,
+      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);
   if (!create_result_or.ok()) {
@@ -2843,8 +3075,11 @@
     TokenizedDocument tokenized_document(
         std::move(tokenized_document_or).ValueOrDie());
 
+    // No valid old_document_id should be used here since we're in recovery mode
+    // and there is no "existing document replacement/update".
     libtextclassifier3::Status status =
-        index_processor.IndexDocument(tokenized_document, document_id);
+        index_processor.IndexDocument(tokenized_document, document_id,
+                                      /*old_document_id=*/kInvalidDocumentId);
     if (!status.ok()) {
       if (!absl_ports::IsDataLoss(status)) {
         // Real error. Stop recovering and pass it up.
@@ -2919,7 +3154,8 @@
   // Embedding index handler
   ICING_ASSIGN_OR_RETURN(
       std::unique_ptr<EmbeddingIndexingHandler> embedding_indexing_handler,
-      EmbeddingIndexingHandler::Create(clock_.get(), embedding_index_.get()));
+      EmbeddingIndexingHandler::Create(clock_.get(), embedding_index_.get(),
+                                       options_.enable_embedding_index()));
   handlers.push_back(std::move(embedding_indexing_handler));
   return handlers;
 }
@@ -3064,13 +3300,13 @@
   // Schema store
   if (rebuild_result.needs_schema_store_derived_files_rebuild) {
     ICING_RETURN_IF_ERROR(SchemaStore::DiscardDerivedFiles(
-        filesystem_.get(), options_.base_dir()));
+        filesystem_.get(), MakeSchemaDirectoryPath(options_.base_dir())));
   }
 
   // Document store
   if (rebuild_result.needs_document_store_derived_files_rebuild) {
     ICING_RETURN_IF_ERROR(DocumentStore::DiscardDerivedFiles(
-        filesystem_.get(), options_.base_dir()));
+        filesystem_.get(), MakeDocumentDirectoryPath(options_.base_dir())));
   }
 
   // Term index
@@ -3098,7 +3334,11 @@
     }
   }
 
-  // TODO(b/326656531): Update version-util to consider embedding index.
+  // Embedding index.
+  if (rebuild_result.needs_embedding_index_rebuild) {
+    ICING_RETURN_IF_ERROR(EmbeddingIndex::Discard(
+        *filesystem_, MakeEmbeddingIndexWorkingPath(options_.base_dir())));
+  }
 
   return libtextclassifier3::Status::OK;
 }
@@ -3181,7 +3421,7 @@
   auto suggestion_processor_or = SuggestionProcessor::Create(
       index_.get(), integer_index_.get(), embedding_index_.get(),
       language_segmenter_.get(), normalizer_.get(), document_store_.get(),
-      schema_store_.get(), clock_.get());
+      schema_store_.get(), clock_.get(), &feature_flags_);
   if (!suggestion_processor_or.ok()) {
     TransformStatus(suggestion_processor_or.status(), response_status);
     return response;
diff --git a/icing/icing-search-engine.h b/icing/icing-search-engine.h
index 299dc6d..d23767d 100644
--- a/icing/icing-search-engine.h
+++ b/icing/icing-search-engine.h
@@ -27,6 +27,7 @@
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/mutex.h"
 #include "icing/absl_ports/thread_annotations.h"
+#include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/version-util.h"
 #include "icing/index/data-indexing-handler.h"
@@ -175,6 +176,21 @@
   //   INTERNAL_ERROR on IO error
   GetSchemaResultProto GetSchema() ICING_LOCKS_EXCLUDED(mutex_);
 
+  // Get Icing's current copy of the schema for the given database.
+  //
+  // NOTE: This is an expensive operation. It is recommended to call GetSchema()
+  // instead if you do not need to filter the schema by database, or if you're
+  // retrieving the only database in the schema.
+  //
+  // Returns:
+  //   SchemaProto on success
+  //   NOT_FOUND if a schema has not been set yet, or if the database is not
+  //     present in the schema
+  //   FAILED_PRECONDITION IcingSearchEngine has not been initialized yet.
+  //   INTERNAL_ERROR on IO error
+  GetSchemaResultProto GetSchema(std::string_view database)
+      ICING_LOCKS_EXCLUDED(mutex_);
+
   // Get Icing's copy of the SchemaTypeConfigProto of name schema_type
   //
   // Returns:
@@ -357,9 +373,21 @@
   // Returns:
   //   File descriptor on success
   //   InvalidArgumentError on invalid blob handle
-  //   PermissionDeniedError on blob is committed
+  //   FailedPreconditionError on blob is already opened for write
+  //   AlreadyExistsError on blob is committed
   //   INTERNAL_ERROR on IO error
-  BlobProto OpenWriteBlob(PropertyProto::BlobHandleProto blob_handle);
+  BlobProto OpenWriteBlob(const PropertyProto::BlobHandleProto& blob_handle);
+
+  // Removes a blob file and blob handle from the blob store.
+  //
+  // This will remove the blob on any state. No matter it's committed or not or
+  // it has reference document links or not.
+  //
+  // Returns:
+  //   InvalidArgumentError on invalid blob handle
+  //   NotFoundError on blob is not found
+  //   InternalError on IO error
+  BlobProto RemoveBlob(const PropertyProto::BlobHandleProto& blob_handle);
 
   // Gets or creates a file for read only purpose for the given blob handle.
   // The blob must be committed by calling commitBlob otherwise it is not
@@ -369,7 +397,7 @@
   //   File descriptor on success
   //   InvalidArgumentError on invalid blob handle
   //   NotFoundError on blob is not found or is not committed
-  BlobProto OpenReadBlob(PropertyProto::BlobHandleProto blob_handle);
+  BlobProto OpenReadBlob(const PropertyProto::BlobHandleProto& blob_handle);
 
   // Commits the given blob, the blob is open to write via openWrite.
   // Before the blob is committed, it is not visible to any reader via openRead.
@@ -381,7 +409,7 @@
   //   False on the blob is already committed.
   //   InvalidArgumentError on invalid blob handle or digest is mismatch with
   //     file content NotFoundError on blob is not found.
-  BlobProto CommitBlob(PropertyProto::BlobHandleProto blob_handle);
+  BlobProto CommitBlob(const PropertyProto::BlobHandleProto& blob_handle);
 
   // Makes sure that every update/delete received till this point is flushed
   // to disk. If the app crashes after a call to PersistToDisk(), Icing
@@ -477,6 +505,7 @@
 
  private:
   const IcingSearchEngineOptions options_;
+  const FeatureFlags feature_flags_;
   const std::unique_ptr<const Filesystem> filesystem_;
   const std::unique_ptr<const IcingFilesystem> icing_filesystem_;
   bool initialized_ ICING_GUARDED_BY(mutex_) = false;
@@ -611,7 +640,7 @@
   //   OK on success
   //   FAILED_PRECONDITION if initialize_stats is null
   libtextclassifier3::Status InitializeBlobStore(
-      int32_t orphan_blob_time_to_live_ms)
+      int32_t orphan_blob_time_to_live_ms, int32_t compression_level)
       ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
 
   // Do any initialization/recovery necessary to create term index, integer
@@ -683,20 +712,15 @@
       QueryStatsProto::SearchStats* search_stats)
       ICING_SHARED_LOCKS_REQUIRED(mutex_);
 
-  // Many of the internal components rely on other components' derived data.
-  // Check that everything is consistent with each other so that we're not
-  // using outdated derived data in some parts of our system.
-  //
-  // NOTE: this method can be called only at startup time or after
-  // PersistToDisk(), otherwise the check could fail due to any changes that are
-  // not persisted.
+  // Deletes documents propagated from the given deleted document ids via
+  // joinable properties with delete propagation enabled.
   //
   // Returns:
-  //   OK on success
-  //   NOT_FOUND if missing header file
-  //   INTERNAL_ERROR on any IO errors or if header is inconsistent
-  libtextclassifier3::Status CheckConsistency()
-      ICING_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+  //   Number of 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_);
 
   // Discards derived data that requires rebuild based on rebuild_result.
   //
diff --git a/icing/icing-search-engine_benchmark.cc b/icing/icing-search-engine_benchmark.cc
index 29dd92a..91924dc 100644
--- a/icing/icing-search-engine_benchmark.cc
+++ b/icing/icing-search-engine_benchmark.cc
@@ -1114,7 +1114,7 @@
   options.set_base_dir(test_dir);
   options.set_index_merge_size(kIcingFullIndexSize);
   options.set_document_store_namespace_id_fingerprint(true);
-  options.set_use_new_qualified_id_join_index(true);
+  options.set_enable_qualified_id_join_index_v3_and_delete_propagate_from(true);
   std::unique_ptr<IcingSearchEngine> icing =
       std::make_unique<IcingSearchEngine>(options);
 
diff --git a/icing/icing-search-engine_blob_test.cc b/icing/icing-search-engine_blob_test.cc
index f32d6d1..72655ae 100644
--- a/icing/icing-search-engine_blob_test.cc
+++ b/icing/icing-search-engine_blob_test.cc
@@ -22,6 +22,7 @@
 #include <utility>
 #include <vector>
 
+#include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "icing/document-builder.h"
 #include "icing/file/filesystem.h"
@@ -29,6 +30,7 @@
 #include "icing/jni/jni-cache.h"
 #include "icing/legacy/index/icing-filesystem.h"
 #include "icing/portable/equals-proto.h"
+#include "icing/proto/storage.pb.h"
 #include "icing/schema-builder.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
@@ -44,7 +46,10 @@
 
 namespace {
 
-using ::testing::Eq;
+using ::icing::lib::portable_equals_proto::EqualsProto;
+using ::testing::IsEmpty;
+using ::testing::SizeIs;
+using ::testing::UnorderedElementsAre;
 
 // For mocking purpose, we allow tests to provide a custom Filesystem.
 class TestIcingSearchEngine : public IcingSearchEngine {
@@ -60,14 +65,16 @@
 };
 
 std::string GetTestBaseDir() { return GetTestTempDir() + "/icing"; }
-std::string GetTestBaseBlobStoreDir() {
-  return GetTestTempDir() + "/icing/blob_dir";
-}
+
+std::string GetTestBlobDir() { return GetTestTempDir() + "/icing/blob_dir"; }
+
+std::string GetTestBlobFileDir() { return GetTestBlobDir() + "/blob_files"; }
 
 // This test is meant to cover all tests relating to IcingSearchEngine::Delete*.
 class IcingSearchEngineBlobTest : public testing::Test {
  protected:
   void SetUp() override {
+    filesystem_.DeleteDirectoryRecursively(GetTestBaseDir().c_str());
     filesystem_.CreateDirectoryRecursively(GetTestBaseDir().c_str());
   }
 
@@ -134,8 +141,8 @@
 
 TEST_F(IcingSearchEngineBlobTest, InvalidBlobHandle) {
   PropertyProto::BlobHandleProto blob_handle;
-  blob_handle.set_label("blob");
   blob_handle.set_digest("invalid");
+  blob_handle.set_namespace_("namespaceA");
 
   IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
@@ -160,10 +167,10 @@
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
 
   PropertyProto::BlobHandleProto blob_handle;
-  blob_handle.set_label("blob");
   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);
   EXPECT_THAT(write_blob_proto.status(),
@@ -181,10 +188,10 @@
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
 
   PropertyProto::BlobHandleProto blob_handle;
-  blob_handle.set_label("label");
   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());
@@ -211,15 +218,90 @@
   }
 }
 
+TEST_F(IcingSearchEngineBlobTest, RemovePendingBlob) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  PropertyProto::BlobHandleProto blob_handle;
+  blob_handle.set_namespace_("namespace1");
+  std::vector<unsigned char> data = GenerateRandomBytes(24);
+  std::array<uint8_t, 32> digest = CalculateDigest(data);
+  blob_handle.set_digest((void*)digest.data(), digest.size());
+
+  BlobProto write_blob_proto = icing.OpenWriteBlob(blob_handle);
+  ASSERT_THAT(write_blob_proto.status(), ProtoIsOk());
+  {
+    ScopedFd write_fd(write_blob_proto.file_descriptor());
+    ASSERT_TRUE(filesystem()->Write(write_fd.get(), data.data(), data.size()));
+  }
+
+  std::vector<std::string> file_names;
+  ASSERT_TRUE(
+      filesystem()->ListDirectory(GetTestBlobFileDir().c_str(), &file_names));
+  ASSERT_THAT(file_names, SizeIs(1));
+
+  EXPECT_THAT(icing.RemoveBlob(blob_handle).status(), ProtoIsOk());
+
+  // Commit will return NOT_FOUND since the blob is removed.
+  BlobProto commit_blob_proto = icing.CommitBlob(blob_handle);
+  EXPECT_THAT(commit_blob_proto.status(),
+              ProtoStatusIs(StatusProto::NOT_FOUND));
+
+  file_names = std::vector<std::string>();
+  ASSERT_TRUE(
+      filesystem()->ListDirectory(GetTestBlobFileDir().c_str(), &file_names));
+  // The pending file is deleted.
+  EXPECT_THAT(file_names, IsEmpty());
+}
+
+TEST_F(IcingSearchEngineBlobTest, RemoveCommittedBlob) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  PropertyProto::BlobHandleProto blob_handle;
+  blob_handle.set_namespace_("namespace1");
+  std::vector<unsigned char> data = GenerateRandomBytes(24);
+  std::array<uint8_t, 32> digest = CalculateDigest(data);
+  blob_handle.set_digest((void*)digest.data(), digest.size());
+
+  BlobProto write_blob_proto = icing.OpenWriteBlob(blob_handle);
+  ASSERT_THAT(write_blob_proto.status(), ProtoIsOk());
+  {
+    ScopedFd write_fd(write_blob_proto.file_descriptor());
+    ASSERT_TRUE(filesystem()->Write(write_fd.get(), data.data(), data.size()));
+  }
+
+  // commit the blob
+  ASSERT_THAT(icing.CommitBlob(blob_handle).status(), ProtoIsOk());
+
+  std::vector<std::string> file_names;
+  ASSERT_TRUE(
+      filesystem()->ListDirectory(GetTestBlobFileDir().c_str(), &file_names));
+  ASSERT_THAT(file_names, SizeIs(1));
+
+  EXPECT_THAT(icing.RemoveBlob(blob_handle).status(), ProtoIsOk());
+
+  // Commit will return NOT_FOUND since the blob is removed.
+  BlobProto commit_blob_proto = icing.CommitBlob(blob_handle);
+  EXPECT_THAT(commit_blob_proto.status(),
+              ProtoStatusIs(StatusProto::NOT_FOUND));
+
+  file_names = std::vector<std::string>();
+  ASSERT_TRUE(
+      filesystem()->ListDirectory(GetTestBlobFileDir().c_str(), &file_names));
+  // The pending file is deleted.
+  EXPECT_THAT(file_names, IsEmpty());
+}
+
 TEST_F(IcingSearchEngineBlobTest, WriteAndReadBlobByDocument) {
   IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
 
   PropertyProto::BlobHandleProto blob_handle;
-  blob_handle.set_label("label");
   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());
@@ -266,11 +348,11 @@
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
 
   PropertyProto::BlobHandleProto blob_handle;
-  blob_handle.set_label("blob1");
 
   std::vector<unsigned char> data = GenerateRandomBytes(24);
   std::array<uint8_t, 32> digest = CalculateDigest(data);
   blob_handle.set_digest(std::string(digest.begin(), digest.end()));
+  blob_handle.set_namespace_("namespaceA");
 
   BlobProto write_blob_proto = icing.OpenWriteBlob(blob_handle);
   ASSERT_THAT(write_blob_proto.status(), ProtoIsOk());
@@ -292,11 +374,11 @@
   EXPECT_THAT(icing1.Initialize().status(), ProtoIsOk());
 
   PropertyProto::BlobHandleProto blob_handle;
-  blob_handle.set_label("blob1");
 
   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 = icing1.OpenWriteBlob(blob_handle);
   ASSERT_THAT(write_blob_proto.status(), ProtoIsOk());
@@ -325,11 +407,11 @@
   ASSERT_THAT(icing1.SetSchema(CreateBlobSchema()).status(), ProtoIsOk());
 
   PropertyProto::BlobHandleProto blob_handle;
-  blob_handle.set_label("blob1");
 
   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 = icing1.OpenWriteBlob(blob_handle);
   ASSERT_THAT(write_blob_proto.status(), ProtoIsOk());
@@ -367,11 +449,11 @@
   ASSERT_THAT(icing1.SetSchema(CreateBlobSchema()).status(), ProtoIsOk());
 
   PropertyProto::BlobHandleProto blob_handle;
-  blob_handle.set_label("blob1");
 
   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 = icing1.OpenWriteBlob(blob_handle);
   ASSERT_THAT(write_blob_proto.status(), ProtoIsOk());
@@ -416,19 +498,11 @@
   // set a schema to icing to avoid wipe out all directories.
   ASSERT_THAT(icing.SetSchema(CreateBlobSchema()).status(), ProtoIsOk());
 
-  std::vector<std::string> file_names;
-  std::unordered_set<std::string> excludes;
-  ASSERT_TRUE(filesystem()->ListDirectory(GetTestBaseBlobStoreDir().c_str(),
-                                          excludes, /*recursive=*/false,
-                                          &file_names));
-  int32_t file_count = file_names.size();
-
   PropertyProto::BlobHandleProto blob_handle;
-  blob_handle.set_label("label");
   std::vector<unsigned char> data = GenerateRandomBytes(24);
   std::array<uint8_t, 32> digest = CalculateDigest(data);
-  std::string digestString = std::string(digest.begin(), digest.end());
-  blob_handle.set_digest(std::move(digestString));
+  blob_handle.set_digest(std::string(digest.begin(), digest.end()));
+  blob_handle.set_namespace_("namespaceA");
 
   BlobProto writeBlobProto = icing.OpenWriteBlob(blob_handle);
   ASSERT_THAT(writeBlobProto.status(), ProtoIsOk());
@@ -437,11 +511,11 @@
     ASSERT_TRUE(filesystem()->Write(write_fd.get(), data.data(), data.size()));
   }
 
-  file_names = std::vector<std::string>();
-  ASSERT_TRUE(filesystem()->ListDirectory(GetTestBaseBlobStoreDir().c_str(),
-                                          excludes, /*recursive=*/false,
-                                          &file_names));
-  ASSERT_THAT(file_names.size(), file_count + 1);
+  std::vector<std::string> file_names;
+  ASSERT_TRUE(
+      filesystem()->ListDirectory(GetTestBlobFileDir().c_str(), &file_names));
+  // The blob file is created.
+  EXPECT_THAT(file_names, SizeIs(1));
 
   BlobProto commitBlobProto = icing.CommitBlob(blob_handle);
   ASSERT_THAT(commitBlobProto.status(), ProtoIsOk());
@@ -473,15 +547,18 @@
   std::string actual_data = std::string(buf.get(), buf.get() + size);
   EXPECT_EQ(expected_data, actual_data);
 
+  file_names = std::vector<std::string>();
+  ASSERT_TRUE(
+      filesystem()->ListDirectory(GetTestBlobDir().c_str(), &file_names));
+  int32_t cur_file_count = file_names.size();
   // Optimize remove the expired orphan blob.
-  ASSERT_THAT(icing2.Optimize().status(), ProtoIsOk());
+  EXPECT_THAT(icing2.Optimize().status(), ProtoIsOk());
   EXPECT_THAT(icing2.OpenReadBlob(blob_handle).status(),
               ProtoStatusIs(StatusProto::NOT_FOUND));
   file_names = std::vector<std::string>();
-  ASSERT_TRUE(filesystem()->ListDirectory(GetTestBaseBlobStoreDir().c_str(),
-                                          excludes, /*recursive=*/false,
-                                          &file_names));
-  ASSERT_THAT(file_names.size(), file_count);
+  ASSERT_TRUE(
+      filesystem()->ListDirectory(GetTestBlobDir().c_str(), &file_names));
+  EXPECT_THAT(file_names, SizeIs(cur_file_count));
 }
 
 TEST_F(IcingSearchEngineBlobTest, BlobOptimizeWithoutCommit) {
@@ -498,11 +575,12 @@
 
   // write two blobs but not commit
   PropertyProto::BlobHandleProto blob_handle1;
-  blob_handle1.set_label("label1");
   std::vector<unsigned char> data1 = GenerateRandomBytes(24);
   std::array<uint8_t, 32> digest1 = CalculateDigest(data1);
-  std::string digestString1 = std::string(digest1.begin(), digest1.end());
-  blob_handle1.set_digest(std::move(digestString1));
+  std::string digest_string1 = std::string(digest1.begin(), digest1.end());
+  blob_handle1.set_digest(std::move(digest_string1));
+  blob_handle1.set_namespace_("namespaceA");
+
   BlobProto writeBlobProto = icing.OpenWriteBlob(blob_handle1);
   ASSERT_THAT(writeBlobProto.status(), ProtoIsOk());
   {
@@ -512,11 +590,10 @@
   }
 
   PropertyProto::BlobHandleProto blob_handle2;
-  blob_handle2.set_label("label2");
   std::vector<unsigned char> data2 = GenerateRandomBytes(24);
   std::array<uint8_t, 32> digest2 = CalculateDigest(data2);
-  std::string digestString2 = std::string(digest2.begin(), digest2.end());
-  blob_handle2.set_digest(std::move(digestString2));
+  blob_handle2.set_digest(std::string(digest2.begin(), digest2.end()));
+  blob_handle2.set_namespace_("namespaceA");
   writeBlobProto = icing.OpenWriteBlob(blob_handle2);
   ASSERT_THAT(writeBlobProto.status(), ProtoIsOk());
   {
@@ -555,11 +632,10 @@
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
 
   PropertyProto::BlobHandleProto blob_handle;
-  blob_handle.set_label("label");
   std::vector<unsigned char> data = GenerateRandomBytes(24);
   std::array<uint8_t, 32> digest = CalculateDigest(data);
-  std::string digestString = std::string(digest.begin(), digest.end());
-  blob_handle.set_digest(std::move(digestString));
+  blob_handle.set_digest(std::string(digest.begin(), digest.end()));
+  blob_handle.set_namespace_("namespaceA");
 
   BlobProto writeBlobProto = icing.OpenWriteBlob(blob_handle);
   ASSERT_THAT(writeBlobProto.status(), ProtoIsOk());
@@ -638,11 +714,10 @@
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
 
   PropertyProto::BlobHandleProto blob_handle;
-  blob_handle.set_label("label");
   std::vector<unsigned char> data = GenerateRandomBytes(24);
   std::array<uint8_t, 32> digest = CalculateDigest(data);
-  std::string digestString = std::string(digest.begin(), digest.end());
-  blob_handle.set_digest(std::move(digestString));
+  blob_handle.set_digest(std::string(digest.begin(), digest.end()));
+  blob_handle.set_namespace_("namespaceA");
 
   BlobProto writeBlobProto = icing.OpenWriteBlob(blob_handle);
   ASSERT_THAT(writeBlobProto.status(), ProtoIsOk());
@@ -750,11 +825,10 @@
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
 
   PropertyProto::BlobHandleProto blob_handle;
-  blob_handle.set_label("label");
   std::vector<unsigned char> data = GenerateRandomBytes(24);
   std::array<uint8_t, 32> digest = CalculateDigest(data);
-  std::string digestString = std::string(digest.begin(), digest.end());
-  blob_handle.set_digest(std::move(digestString));
+  blob_handle.set_digest(std::string(digest.begin(), digest.end()));
+  blob_handle.set_namespace_("namespaceA");
 
   BlobProto writeBlobProto = icing.OpenWriteBlob(blob_handle);
   ASSERT_THAT(writeBlobProto.status(), ProtoIsOk());
@@ -845,19 +919,12 @@
                               std::move(fake_clock), GetTestJniCache());
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
 
-  std::vector<std::string> file_names;
-  std::unordered_set<std::string> excludes;
-  ASSERT_TRUE(filesystem()->ListDirectory(GetTestBaseBlobStoreDir().c_str(),
-                                          excludes, /*recursive=*/false,
-                                          &file_names));
-  int32_t file_count = file_names.size();
-
   PropertyProto::BlobHandleProto blob_handle1;
-  blob_handle1.set_label("label1");
   std::vector<unsigned char> data1 = GenerateRandomBytes(24);
   std::array<uint8_t, 32> digest1 = CalculateDigest(data1);
-  std::string digestString1 = std::string(digest1.begin(), digest1.end());
-  blob_handle1.set_digest(std::move(digestString1));
+  std::string digest_string1 = std::string(digest1.begin(), digest1.end());
+  blob_handle1.set_digest(std::move(digest_string1));
+  blob_handle1.set_namespace_("namespaceA");
 
   BlobProto writeBlobProto1 = icing.OpenWriteBlob(blob_handle1);
   ASSERT_THAT(writeBlobProto1.status(), ProtoIsOk());
@@ -871,11 +938,10 @@
   ASSERT_THAT(commitBlobProto.status(), ProtoIsOk());
 
   PropertyProto::BlobHandleProto blob_handle2;
-  blob_handle2.set_label("label2");
   std::vector<unsigned char> data2 = GenerateRandomBytes(24);
   std::array<uint8_t, 32> digest2 = CalculateDigest(data2);
-  std::string digestString2 = std::string(digest2.begin(), digest2.end());
-  blob_handle2.set_digest(std::move(digestString2));
+  blob_handle2.set_digest(std::string(digest2.begin(), digest2.end()));
+  blob_handle2.set_namespace_("namespaceA");
 
   BlobProto writeBlobProto2 = icing.OpenWriteBlob(blob_handle2);
   ASSERT_THAT(writeBlobProto2.status(), ProtoIsOk());
@@ -889,11 +955,10 @@
   ASSERT_THAT(commitBlobProto2.status(), ProtoIsOk());
 
   PropertyProto::BlobHandleProto blob_handle3;
-  blob_handle3.set_label("label3");
   std::vector<unsigned char> data3 = GenerateRandomBytes(24);
   std::array<uint8_t, 32> digest3 = CalculateDigest(data3);
-  std::string digestString3 = std::string(digest3.begin(), digest3.end());
-  blob_handle3.set_digest(std::move(digestString3));
+  blob_handle3.set_digest(std::string(digest3.begin(), digest3.end()));
+  blob_handle3.set_namespace_("namespaceA");
 
   BlobProto writeBlobProto3 = icing.OpenWriteBlob(blob_handle3);
   ASSERT_THAT(writeBlobProto3.status(), ProtoIsOk());
@@ -906,12 +971,11 @@
   BlobProto commitBlobProto3 = icing.CommitBlob(blob_handle3);
   ASSERT_THAT(commitBlobProto3.status(), ProtoIsOk());
 
-  file_names = std::vector<std::string>();
-  ASSERT_TRUE(filesystem()->ListDirectory(GetTestBaseBlobStoreDir().c_str(),
-                                          excludes, /*recursive=*/false,
-                                          &file_names));
-  ASSERT_THAT(file_names.size(), file_count + 3);
-
+  std::vector<std::string> file_names;
+  ASSERT_TRUE(
+      filesystem()->ListDirectory(GetTestBlobFileDir().c_str(), &file_names));
+  // 3 more blob files are created.
+  ASSERT_THAT(file_names, SizeIs(3));
   // Set schema and put 3 documents that contains the blob handle
   ASSERT_THAT(icing.SetSchema(CreateBlobSchema()).status(), ProtoIsOk());
   ASSERT_THAT(
@@ -955,10 +1019,11 @@
               ProtoStatusIs(StatusProto::NOT_FOUND));
   ASSERT_THAT(icing2.OpenReadBlob(blob_handle3).status(), ProtoIsOk());
   file_names = std::vector<std::string>();
-  ASSERT_TRUE(filesystem()->ListDirectory(GetTestBaseBlobStoreDir().c_str(),
-                                          excludes, /*recursive=*/false,
-                                          &file_names));
-  ASSERT_THAT(file_names.size(), file_count + 1);
+  ASSERT_TRUE(
+      filesystem()->ListDirectory(GetTestBlobFileDir().c_str(), &file_names));
+
+  // 2 blob files are removed
+  ASSERT_THAT(file_names, SizeIs(1));
 
   // remove the last reference document, now the all blobs become orphan.
   ASSERT_THAT(icing2.Delete("namespace", "doc3").status(), ProtoIsOk());
@@ -967,10 +1032,10 @@
   EXPECT_THAT(icing2.OpenReadBlob(blob_handle3).status(),
               ProtoStatusIs(StatusProto::NOT_FOUND));
   file_names = std::vector<std::string>();
-  ASSERT_TRUE(filesystem()->ListDirectory(GetTestBaseBlobStoreDir().c_str(),
-                                          excludes, /*recursive=*/false,
-                                          &file_names));
-  ASSERT_THAT(file_names.size(), file_count);
+  ASSERT_TRUE(
+      filesystem()->ListDirectory(GetTestBlobFileDir().c_str(), &file_names));
+  // the last blob file is removed.
+  ASSERT_THAT(file_names, SizeIs(0));
 }
 
 TEST_F(IcingSearchEngineBlobTest, OptimizeBlobHandlesNoTTL) {
@@ -990,11 +1055,10 @@
   ASSERT_THAT(icing.SetSchema(CreateBlobSchema()).status(), ProtoIsOk());
 
   PropertyProto::BlobHandleProto blob_handle;
-  blob_handle.set_label("label");
   std::vector<unsigned char> data = GenerateRandomBytes(24);
   std::array<uint8_t, 32> digest = CalculateDigest(data);
-  std::string digestString = std::string(digest.begin(), digest.end());
-  blob_handle.set_digest(std::move(digestString));
+  blob_handle.set_digest(std::string(digest.begin(), digest.end()));
+  blob_handle.set_namespace_("namespaceA");
 
   BlobProto writeBlobProto = icing.OpenWriteBlob(blob_handle);
   ASSERT_THAT(writeBlobProto.status(), ProtoIsOk());
@@ -1033,6 +1097,128 @@
   EXPECT_EQ(expected_data, actual_data);
 }
 
+TEST_F(IcingSearchEngineBlobTest, EmptyNamespace) {
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetSystemTimeMilliseconds(1000);
+  TestIcingSearchEngine icing(GetDefaultIcingOptions(),
+                              std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  PropertyProto::BlobHandleProto blob_handle;
+  std::vector<unsigned char> data = GenerateRandomBytes(12);
+  std::array<uint8_t, 32> digest = CalculateDigest(data);
+  blob_handle.set_digest(std::string(digest.begin(), digest.end()));
+  BlobProto writeBlobProto = icing.OpenWriteBlob(blob_handle);
+
+  EXPECT_THAT(writeBlobProto.status(),
+              ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
+}
+
+TEST_F(IcingSearchEngineBlobTest, OptimizeNamespaceUsage) {
+  auto fake_clock = std::make_unique<FakeClock>();
+  fake_clock->SetSystemTimeMilliseconds(1000);
+  TestIcingSearchEngine icing(GetDefaultIcingOptions(),
+                              std::make_unique<Filesystem>(),
+                              std::make_unique<IcingFilesystem>(),
+                              std::move(fake_clock), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  // insert 3 blobs from 3 different namespaces
+  PropertyProto::BlobHandleProto blob_handle1;
+  std::vector<unsigned char> data1 = GenerateRandomBytes(12);
+  std::array<uint8_t, 32> digest1 = CalculateDigest(data1);
+  blob_handle1.set_digest(std::string(digest1.begin(), digest1.end()));
+  blob_handle1.set_namespace_("namespaceA");
+  BlobProto writeBlobProto1 = icing.OpenWriteBlob(blob_handle1);
+  ASSERT_THAT(writeBlobProto1.status(), ProtoIsOk());
+  {
+    ScopedFd write_fd(writeBlobProto1.file_descriptor());
+    ASSERT_TRUE(
+        filesystem()->Write(write_fd.get(), data1.data(), data1.size()));
+  }
+  BlobProto commitBlobProto = icing.CommitBlob(blob_handle1);
+  ASSERT_THAT(commitBlobProto.status(), ProtoIsOk());
+
+  PropertyProto::BlobHandleProto blob_handle2;
+  std::vector<unsigned char> data2 = GenerateRandomBytes(24);
+  std::array<uint8_t, 32> digest2 = CalculateDigest(data2);
+  blob_handle2.set_digest(std::string(digest2.begin(), digest2.end()));
+  blob_handle2.set_namespace_("namespaceB");
+  BlobProto writeBlobProto2 = icing.OpenWriteBlob(blob_handle2);
+  ASSERT_THAT(writeBlobProto2.status(), ProtoIsOk());
+  {
+    ScopedFd write_fd(writeBlobProto2.file_descriptor());
+    ASSERT_TRUE(
+        filesystem()->Write(write_fd.get(), data2.data(), data2.size()));
+  }
+  BlobProto commitBlobProto2 = icing.CommitBlob(blob_handle2);
+  ASSERT_THAT(commitBlobProto2.status(), ProtoIsOk());
+
+  PropertyProto::BlobHandleProto blob_handle3;
+  std::vector<unsigned char> data3 = GenerateRandomBytes(36);
+  std::array<uint8_t, 32> digest3 = CalculateDigest(data3);
+  blob_handle3.set_digest(std::string(digest3.begin(), digest3.end()));
+  blob_handle3.set_namespace_("namespaceC");
+  BlobProto writeBlobProto3 = icing.OpenWriteBlob(blob_handle3);
+  ASSERT_THAT(writeBlobProto3.status(), ProtoIsOk());
+  {
+    ScopedFd write_fd(writeBlobProto3.file_descriptor());
+    ASSERT_TRUE(
+        filesystem()->Write(write_fd.get(), data3.data(), data3.size()));
+  }
+  BlobProto commitBlobProto3 = icing.CommitBlob(blob_handle3);
+  ASSERT_THAT(commitBlobProto3.status(), ProtoIsOk());
+
+  // Set schema and put a documents that contains the blob handle2 only
+  ASSERT_THAT(icing.SetSchema(CreateBlobSchema()).status(), ProtoIsOk());
+  ASSERT_THAT(
+      icing.Put(CreateBlobDocument("namespace", "doc", blob_handle2)).status(),
+      ProtoIsOk());
+
+  // persist blob to disk
+  EXPECT_THAT(icing.PersistToDisk(PersistType::FULL).status(), ProtoIsOk());
+
+  // Verify namespace usage
+  StorageInfoResultProto storage_info_result = icing.GetStorageInfo();
+  EXPECT_THAT(storage_info_result.status(), ProtoIsOk());
+  NamespaceBlobStorageInfoProto namespace_info_a;
+  namespace_info_a.set_namespace_("namespaceA");
+  namespace_info_a.set_blob_size(12);
+  namespace_info_a.set_num_blobs(1);
+  NamespaceBlobStorageInfoProto namespace_info_b;
+  namespace_info_b.set_namespace_("namespaceB");
+  namespace_info_b.set_blob_size(24);
+  namespace_info_b.set_num_blobs(1);
+  NamespaceBlobStorageInfoProto namespace_info_c;
+  namespace_info_c.set_namespace_("namespaceC");
+  namespace_info_c.set_blob_size(36);
+  namespace_info_c.set_num_blobs(1);
+  EXPECT_THAT(storage_info_result.storage_info().namespace_blob_storage_info(),
+              UnorderedElementsAre(EqualsProto(namespace_info_a),
+                                   EqualsProto(namespace_info_b),
+                                   EqualsProto(namespace_info_c)));
+
+  // create second icing in 8 days later
+  auto fake_clock2 = std::make_unique<FakeClock>();
+  fake_clock2->SetSystemTimeMilliseconds(1000 + 8 * 24 * 60 * 60 *
+                                                    1000);  // pass 8 days
+  TestIcingSearchEngine icing2(GetDefaultIcingOptions(),
+                               std::make_unique<Filesystem>(),
+                               std::make_unique<IcingFilesystem>(),
+                               std::move(fake_clock2), GetTestJniCache());
+  ASSERT_THAT(icing2.Initialize().status(), ProtoIsOk());
+
+  // After optimize, blobs of namespaceA and namespaceC are removed.
+  ASSERT_THAT(icing2.Optimize().status(), ProtoIsOk());
+
+  storage_info_result = icing2.GetStorageInfo();
+  EXPECT_THAT(storage_info_result.status(), ProtoIsOk());
+  EXPECT_THAT(storage_info_result.storage_info().namespace_blob_storage_info(),
+              UnorderedElementsAre(EqualsProto(namespace_info_b)));
+}
+
 }  // namespace
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/icing-search-engine_delete_test.cc b/icing/icing-search-engine_delete_test.cc
index 6546d2d..b85654f 100644
--- a/icing/icing-search-engine_delete_test.cc
+++ b/icing/icing-search-engine_delete_test.cc
@@ -47,10 +47,10 @@
 #include "icing/schema-builder.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/jni-test-helpers.h"
 #include "icing/testing/test-data.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/icu-data-file-helper.h"
 
 namespace icing {
 namespace lib {
@@ -97,7 +97,7 @@
       std::string icu_data_file_path =
           GetTestFilePath("icing/icu.dat");
       ICING_ASSERT_OK(
-          icu_data_file_helper::SetUpICUDataFile(icu_data_file_path));
+          icu_data_file_helper::SetUpIcuDataFile(icu_data_file_path));
     }
     filesystem_.CreateDirectoryRecursively(GetTestBaseDir().c_str());
   }
@@ -118,33 +118,32 @@
 IcingSearchEngineOptions GetDefaultIcingOptions() {
   IcingSearchEngineOptions icing_options;
   icing_options.set_base_dir(GetTestBaseDir());
+  icing_options.set_enable_qualified_id_join_index_v3_and_delete_propagate_from(
+      true);
   return icing_options;
 }
 
-SchemaProto CreateMessageSchema() {
-  return SchemaBuilder()
-      .AddType(SchemaTypeConfigBuilder().SetType("Message").AddProperty(
-          PropertyConfigBuilder()
-              .SetName("body")
-              .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
-              .SetCardinality(CARDINALITY_REQUIRED)))
+SchemaTypeConfigProto CreateMessageSchemaTypeConfig() {
+  return SchemaTypeConfigBuilder()
+      .SetType("Message")
+      .AddProperty(PropertyConfigBuilder()
+                       .SetName("body")
+                       .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                       .SetCardinality(CARDINALITY_REQUIRED))
       .Build();
 }
 
-SchemaProto CreateEmailSchema() {
-  return SchemaBuilder()
-      .AddType(SchemaTypeConfigBuilder()
-                   .SetType("Email")
-                   .AddProperty(PropertyConfigBuilder()
-                                    .SetName("body")
-                                    .SetDataTypeString(TERM_MATCH_PREFIX,
-                                                       TOKENIZER_PLAIN)
-                                    .SetCardinality(CARDINALITY_REQUIRED))
-                   .AddProperty(PropertyConfigBuilder()
-                                    .SetName("subject")
-                                    .SetDataTypeString(TERM_MATCH_PREFIX,
-                                                       TOKENIZER_PLAIN)
-                                    .SetCardinality(CARDINALITY_REQUIRED)))
+SchemaTypeConfigProto CreateEmailSchemaTypeConfig() {
+  return SchemaTypeConfigBuilder()
+      .SetType("Email")
+      .AddProperty(PropertyConfigBuilder()
+                       .SetName("body")
+                       .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                       .SetCardinality(CARDINALITY_REQUIRED))
+      .AddProperty(PropertyConfigBuilder()
+                       .SetName("subject")
+                       .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                       .SetCardinality(CARDINALITY_REQUIRED))
       .Build();
 }
 
@@ -154,6 +153,186 @@
   return scoring_spec;
 }
 
+TEST_F(IcingSearchEngineDeleteTest, Delete) {
+  SchemaProto schema =
+      SchemaBuilder().AddType(CreateMessageSchemaTypeConfig()).Build();
+
+  DocumentProto document =
+      DocumentBuilder()
+          .SetKey("namespace", "uri")
+          .SetSchema("Message")
+          .AddStringProperty("body", "message body1")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  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_proto;
+  expected_get_result_proto.mutable_status()->set_code(StatusProto::OK);
+  *expected_get_result_proto.mutable_document() = document;
+  ASSERT_THAT(
+      icing.Get("namespace", "uri", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto));
+
+  // Delete "namespace", "uri".
+  EXPECT_THAT(icing.Delete("namespace", "uri").status(), ProtoIsOk());
+
+  // Get again.
+  expected_get_result_proto.mutable_status()->set_code(StatusProto::NOT_FOUND);
+  expected_get_result_proto.mutable_status()->set_message(
+      "Document (namespace, uri) not found.");
+  expected_get_result_proto.clear_document();
+  EXPECT_THAT(
+      icing.Get("namespace", "uri", GetResultSpecProto::default_instance()),
+      EqualsProto(expected_get_result_proto));
+}
+
+TEST_F(IcingSearchEngineDeleteTest, DeleteWithJoinDeletePropagation) {
+  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)))
+          .Build();
+
+  DocumentProto person1 =
+      DocumentBuilder()
+          .SetKey("namespace", "person1")
+          .SetSchema("Person")
+          .AddStringProperty("name", "Alice")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto person2 =
+      DocumentBuilder()
+          .SetKey("namespace", "person2")
+          .SetSchema("Person")
+          .AddStringProperty("name", "Bob")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto email1 =
+      DocumentBuilder()
+          .SetKey("namespace", "email1")
+          .SetSchema("Email")
+          .AddStringProperty("subject", "test")
+          .AddStringProperty("sender", "namespace#person1")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto email2 =
+      DocumentBuilder()
+          .SetKey("namespace", "email2")
+          .SetSchema("Email")
+          .AddStringProperty("subject", "test")
+          .AddStringProperty("sender", "namespace#person2")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto message1 =
+      DocumentBuilder()
+          .SetKey("namespace", "message1")
+          .SetSchema("Message")
+          .AddStringProperty("body", "test")
+          .AddStringProperty("sender", "namespace#person1")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto message2 =
+      DocumentBuilder()
+          .SetKey("namespace", "message2")
+          .SetSchema("Message")
+          .AddStringProperty("body", "test")
+          .AddStringProperty("sender", "namespace#person2")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+
+  IcingSearchEngine icing(GetDefaultIcingOptions(), 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(email1).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email2).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(message1).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(message2).status(), ProtoIsOk());
+
+  // Delete person1.
+  DeleteResultProto delete_result = icing.Delete("namespace", "person1");
+  EXPECT_THAT(delete_result.status(), ProtoIsOk());
+  // Person1 and email1 should be deleted.
+  EXPECT_THAT(delete_result.delete_stats().num_documents_deleted(), Eq(2));
+
+  // Verify Get API for email and message documents.
+  // 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));
+}
+
 TEST_F(IcingSearchEngineDeleteTest, DeleteBySchemaType) {
   SchemaProto schema;
   // Add an email type
@@ -258,22 +437,22 @@
 }
 
 TEST_F(IcingSearchEngineDeleteTest, DeleteSchemaTypeByQuery) {
-  SchemaProto schema = CreateMessageSchema();
-  // Add an email type
-  SchemaProto tmp = CreateEmailSchema();
-  *schema.add_types() = tmp.types(0);
+  SchemaProto schema = SchemaBuilder()
+                           .AddType(CreateMessageSchemaTypeConfig())
+                           .AddType(CreateEmailSchemaTypeConfig())
+                           .Build();
 
   DocumentProto document1 =
       DocumentBuilder()
           .SetKey("namespace1", "uri1")
-          .SetSchema(schema.types(0).schema_type())
+          .SetSchema("Message")
           .AddStringProperty("body", "message body1")
           .SetCreationTimestampMs(kDefaultCreationTimestampMs)
           .Build();
   DocumentProto document2 =
       DocumentBuilder()
           .SetKey("namespace2", "uri2")
-          .SetSchema(schema.types(1).schema_type())
+          .SetSchema("Email")
           .AddStringProperty("subject", "subject subject2")
           .AddStringProperty("body", "message body2")
           .SetCreationTimestampMs(kDefaultCreationTimestampMs)
@@ -299,7 +478,7 @@
   // Delete the first type. The first doc should be irretrievable. The
   // second should still be present.
   SearchSpecProto search_spec;
-  search_spec.add_schema_type_filters(schema.types(0).schema_type());
+  search_spec.add_schema_type_filters("Message");
   EXPECT_THAT(icing.DeleteByQuery(search_spec).status(), ProtoIsOk());
 
   expected_get_result_proto.mutable_status()->set_code(StatusProto::NOT_FOUND);
@@ -333,6 +512,9 @@
 }
 
 TEST_F(IcingSearchEngineDeleteTest, DeleteByNamespace) {
+  SchemaProto schema =
+      SchemaBuilder().AddType(CreateMessageSchemaTypeConfig()).Build();
+
   DocumentProto document1 =
       DocumentBuilder()
           .SetKey("namespace1", "uri1")
@@ -362,7 +544,7 @@
                               std::make_unique<IcingFilesystem>(),
                               std::move(fake_clock), GetTestJniCache());
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
-  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).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.Put(document3).status(), ProtoIsOk());
@@ -434,6 +616,9 @@
 }
 
 TEST_F(IcingSearchEngineDeleteTest, DeleteNamespaceByQuery) {
+  SchemaProto schema =
+      SchemaBuilder().AddType(CreateMessageSchemaTypeConfig()).Build();
+
   DocumentProto document1 =
       DocumentBuilder()
           .SetKey("namespace1", "uri1")
@@ -451,7 +636,7 @@
 
   IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
   EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
-  EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
   EXPECT_THAT(icing.Put(document1).status(), ProtoIsOk());
   EXPECT_THAT(icing.Put(document2).status(), ProtoIsOk());
 
@@ -504,6 +689,9 @@
 }
 
 TEST_F(IcingSearchEngineDeleteTest, DeleteByQuery) {
+  SchemaProto schema =
+      SchemaBuilder().AddType(CreateMessageSchemaTypeConfig()).Build();
+
   DocumentProto document1 =
       DocumentBuilder()
           .SetKey("namespace1", "uri1")
@@ -526,7 +714,7 @@
                               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.SetSchema(schema).status(), ProtoIsOk());
   EXPECT_THAT(icing.Put(document1).status(), ProtoIsOk());
   EXPECT_THAT(icing.Put(document2).status(), ProtoIsOk());
 
@@ -591,6 +779,9 @@
 }
 
 TEST_F(IcingSearchEngineDeleteTest, DeleteByQueryReturnInfo) {
+  SchemaProto schema =
+      SchemaBuilder().AddType(CreateMessageSchemaTypeConfig()).Build();
+
   DocumentProto document1 =
       DocumentBuilder()
           .SetKey("namespace1", "uri1")
@@ -620,7 +811,7 @@
                               std::make_unique<IcingFilesystem>(),
                               std::move(fake_clock), GetTestJniCache());
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
-  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).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.Put(document3).status(), ProtoIsOk());
@@ -690,6 +881,9 @@
 }
 
 TEST_F(IcingSearchEngineDeleteTest, DeleteByQueryNotFound) {
+  SchemaProto schema =
+      SchemaBuilder().AddType(CreateMessageSchemaTypeConfig()).Build();
+
   DocumentProto document1 =
       DocumentBuilder()
           .SetKey("namespace1", "uri1")
@@ -707,7 +901,7 @@
 
   IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
   EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
-  EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
   EXPECT_THAT(icing.Put(document1).status(), ProtoIsOk());
   EXPECT_THAT(icing.Put(document2).status(), ProtoIsOk());
 
diff --git a/icing/icing-search-engine_fuzz_test.cc b/icing/icing-search-engine_fuzz_test.cc
index 2cf19ad..6f1cf69 100644
--- a/icing/icing-search-engine_fuzz_test.cc
+++ b/icing/icing-search-engine_fuzz_test.cc
@@ -26,9 +26,9 @@
 #include "icing/proto/search.pb.h"
 #include "icing/proto/term.pb.h"
 #include "icing/schema-builder.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/test-data.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/icu-data-file-helper.h"
 
 namespace icing {
 namespace lib {
@@ -64,7 +64,7 @@
   // 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()) {
+  if (!icu_data_file_helper::SetUpIcuDataFile(icu_data_file_path).ok()) {
     return 1;
   }
   IcingSearchEngine icing(icing_options);
diff --git a/icing/icing-search-engine_initialization_test.cc b/icing/icing-search-engine_initialization_test.cc
index 3ffc2cf..7643e4c 100644
--- a/icing/icing-search-engine_initialization_test.cc
+++ b/icing/icing-search-engine_initialization_test.cc
@@ -28,6 +28,7 @@
 #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"
@@ -36,6 +37,8 @@
 #include "icing/file/version-util.h"
 #include "icing/icing-search-engine.h"
 #include "icing/index/data-indexing-handler.h"
+#include "icing/index/embed/embedding-index.h"
+#include "icing/index/embedding-indexing-handler.h"
 #include "icing/index/index-processor.h"
 #include "icing/index/index.h"
 #include "icing/index/integer-section-indexing-handler.h"
@@ -44,8 +47,9 @@
 #include "icing/index/numeric/numeric-index.h"
 #include "icing/index/term-indexing-handler.h"
 #include "icing/jni/jni-cache.h"
+#include "icing/join/document-join-id-pair.h"
 #include "icing/join/join-processor.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/legacy/index/icing-filesystem.h"
@@ -77,18 +81,20 @@
 #include "icing/store/document-id.h"
 #include "icing/store/document-log-creator.h"
 #include "icing/store/document-store.h"
-#include "icing/store/namespace-fingerprint-identifier.h"
+#include "icing/store/namespace-id-fingerprint.h"
 #include "icing/testing/common-matchers.h"
+#include "icing/testing/embedding-test-utils.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/jni-test-helpers.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/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
 #include "icing/util/clock.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "icing/util/tokenized-document.h"
 #include "unicode/uloc.h"
 
@@ -107,6 +113,7 @@
 using ::testing::IsEmpty;
 using ::testing::Matcher;
 using ::testing::Ne;
+using ::testing::Pointee;
 using ::testing::Return;
 using ::testing::SizeIs;
 
@@ -162,6 +169,8 @@
 class IcingSearchEngineInitializationTest : public testing::Test {
  protected:
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+
     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.
@@ -172,7 +181,7 @@
       std::string icu_data_file_path =
           GetTestFilePath("icing/icu.dat");
       ICING_ASSERT_OK(
-          icu_data_file_helper::SetUpICUDataFile(icu_data_file_path));
+          icu_data_file_helper::SetUpIcuDataFile(icu_data_file_path));
     }
     filesystem_.CreateDirectoryRecursively(GetTestBaseDir().c_str());
 
@@ -197,6 +206,7 @@
 
   const IcingFilesystem* icing_filesystem() const { return &icing_filesystem_; }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   IcingFilesystem icing_filesystem_;
   std::unique_ptr<LanguageSegmenter> lang_segmenter_;
@@ -220,6 +230,10 @@
   return GetTestBaseDir() + "/qualified_id_join_index_dir";
 }
 
+std::string GetEmbeddingIndexDir() {
+  return GetTestBaseDir() + "/embedding_index_dir";
+}
+
 std::string GetSchemaDir() { return GetTestBaseDir() + "/schema_dir"; }
 
 std::string GetBlobDir() { return GetTestBaseDir() + "/blob_dir"; }
@@ -232,7 +246,11 @@
   IcingSearchEngineOptions icing_options;
   icing_options.set_base_dir(GetTestBaseDir());
   icing_options.set_document_store_namespace_id_fingerprint(true);
-  icing_options.set_use_new_qualified_id_join_index(true);
+  icing_options.set_enable_embedding_index(true);
+  icing_options.set_enable_embedding_quantization(true);
+  icing_options.set_enable_blob_store(true);
+  icing_options.set_enable_qualified_id_join_index_v3_and_delete_propagate_from(
+      true);
   return icing_options;
 }
 
@@ -919,7 +937,8 @@
     FakeClock fake_clock;
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(filesystem(), GetSchemaDir(), &fake_clock));
+        SchemaStore::Create(filesystem(), GetSchemaDir(), &fake_clock,
+                            feature_flags_.get()));
     ICING_EXPECT_OK(schema_store->SetSchema(
         new_schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -1076,24 +1095,26 @@
     FakeClock fake_clock;
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(filesystem(), GetSchemaDir(), &fake_clock));
+        SchemaStore::Create(filesystem(), GetSchemaDir(), &fake_clock,
+                            feature_flags_.get()));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         BlobStore blob_store,
         BlobStore::Create(filesystem(), GetBlobDir(), &fake_clock,
-                          /*orphan_blob_time_to_live_ms=*/0));
+                          /*orphan_blob_time_to_live_ms=*/0,
+                          PortableFileBackedProtoLog<
+                              BlobInfoProto>::kDefaultCompressionLevel));
 
     // 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(),
+                              schema_store.get(), feature_flags_.get(),
                               /*force_recovery_and_revalidate_documents=*/false,
-                              /*namespace_id_fingerprint=*/true,
                               /*pre_mapping_fbv=*/false,
                               /*use_persistent_hash_map=*/true,
                               PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDeflateCompressionLevel,
+                                  DocumentWrapper>::kDefaultCompressionLevel,
                               /*initialize_stats=*/nullptr));
     std::unique_ptr<DocumentStore> document_store =
         std::move(create_result.document_store);
@@ -1124,8 +1145,9 @@
   EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
                                     EndsWith("/qualified_id_join_index_dir")))
       .Times(0);
-  EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
-                                    HasSubstr("/qualified_id_join_index_dir/")))
+  EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+  EXPECT_CALL(*mock_filesystem,
+              DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
       .Times(0);
 
   TestIcingSearchEngine icing(icing_options, std::move(mock_filesystem),
@@ -1226,14 +1248,12 @@
 
 TEST_F(IcingSearchEngineInitializationTest, RecoverFromCorruptedDocumentStore) {
   // Test the following scenario: some document store derived files are
-  // corrupted. IcingSearchEngine should be able to recover the document store,
-  // and since NamespaceIds were reassigned, we should rebuild qualified id join
-  // index as well. Several additional behaviors are also tested:
+  // corrupted. IcingSearchEngine should be able to recover the document store.
+  // Several additional behaviors are also tested:
   // - Index directory handling:
   //   - Term index directory should be unaffected.
   //   - Integer index directory should be unaffected.
-  //   - Should discard the entire qualified id join index directory and start
-  //     it from scratch.
+  //   - Qualified id join index directory should be unaffected.
   // - Truncate indices:
   //   - "TruncateTo()" for term index shouldn't take effect.
   //   - "Clear()" shouldn't be called for integer index, i.e. no integer index
@@ -1328,7 +1348,8 @@
     FakeClock fake_clock;
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(filesystem(), GetSchemaDir(), &fake_clock));
+        SchemaStore::Create(filesystem(), GetSchemaDir(), &fake_clock,
+                            feature_flags_.get()));
 
     // Manually corrupt one of the derived files of DocumentStore without
     // updating checksum in DocumentStore header.
@@ -1367,14 +1388,15 @@
   EXPECT_CALL(*mock_filesystem,
               DeleteDirectoryRecursively(HasSubstr("/integer_index_dir/")))
       .Times(0);
-  // Ensure qualified id join index directory should be discarded once, and
+  // Ensure qualified id join index directory should never be discarded, and
   // Clear() should never be called (i.e. storage sub directory
   // "*/qualified_id_join_index_dir/*" should never be discarded).
   EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
                                     EndsWith("/qualified_id_join_index_dir")))
-      .Times(1);
-  EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
-                                    HasSubstr("/qualified_id_join_index_dir/")))
+      .Times(0);
+  EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+  EXPECT_CALL(*mock_filesystem,
+              DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
       .Times(0);
 
   TestIcingSearchEngine icing(icing_options, std::move(mock_filesystem),
@@ -1388,16 +1410,15 @@
   EXPECT_THAT(
       initialize_result.initialize_stats().document_store_recovery_cause(),
       Eq(InitializeStatsProto::IO_ERROR));
-  // Term, integer index should be unaffected.
+  // Term, integer index and qualified id join index should be unaffected.
   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));
-  // Qualified id join index should be rebuilt.
   EXPECT_THAT(initialize_result.initialize_stats()
                   .qualified_id_join_index_restoration_cause(),
-              Eq(InitializeStatsProto::DEPENDENCIES_CHANGED));
+              Eq(InitializeStatsProto::NONE));
 
   // Verify join search: join a query for `name:person` with a child query for
   // `body:message` based on the child's `senderQualifiedId` field. message2
@@ -1558,8 +1579,9 @@
   EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
                                     EndsWith("/qualified_id_join_index_dir")))
       .Times(0);
-  EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
-                                    HasSubstr("/qualified_id_join_index_dir/")))
+  EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+  EXPECT_CALL(*mock_filesystem,
+              DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
       .Times(0);
 
   TestIcingSearchEngine icing(GetDefaultIcingOptions(),
@@ -1704,8 +1726,9 @@
   EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
                                     EndsWith("/qualified_id_join_index_dir")))
       .Times(0);
-  EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
-                                    HasSubstr("/qualified_id_join_index_dir/")))
+  EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+  EXPECT_CALL(*mock_filesystem,
+              DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
       .Times(0);
 
   TestIcingSearchEngine icing(GetDefaultIcingOptions(),
@@ -1791,9 +1814,9 @@
     EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
                                       EndsWith("/qualified_id_join_index_dir")))
         .Times(0);
-    EXPECT_CALL(
-        *mock_filesystem,
-        DeleteDirectoryRecursively(HasSubstr("/qualified_id_join_index_dir/")))
+    EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+    EXPECT_CALL(*mock_filesystem,
+                DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
         .Times(0);
 
     static constexpr int32_t kNewIntegerIndexBucketSplitThreshold = 1000;
@@ -1972,8 +1995,9 @@
   EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
                                     EndsWith("/qualified_id_join_index_dir")))
       .Times(1);
-  EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
-                                    HasSubstr("/qualified_id_join_index_dir/")))
+  EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+  EXPECT_CALL(*mock_filesystem,
+              DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
       .Times(0);
 
   TestIcingSearchEngine icing(GetDefaultIcingOptions(),
@@ -2121,9 +2145,9 @@
     EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
                                       EndsWith("/qualified_id_join_index_dir")))
         .Times(0);
-    EXPECT_CALL(
-        *mock_filesystem,
-        DeleteDirectoryRecursively(HasSubstr("/qualified_id_join_index_dir/")))
+    EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+    EXPECT_CALL(*mock_filesystem,
+                DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
         .Times(0);
 
     TestIcingSearchEngine icing(
@@ -2315,9 +2339,9 @@
     EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
                                       EndsWith("/qualified_id_join_index_dir")))
         .Times(0);
-    EXPECT_CALL(
-        *mock_filesystem,
-        DeleteDirectoryRecursively(HasSubstr("/qualified_id_join_index_dir/")))
+    EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+    EXPECT_CALL(*mock_filesystem,
+                DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
         .Times(0);
 
     TestIcingSearchEngine icing(
@@ -2511,9 +2535,9 @@
     EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
                                       EndsWith("/qualified_id_join_index_dir")))
         .Times(0);
-    EXPECT_CALL(
-        *mock_filesystem,
-        DeleteDirectoryRecursively(HasSubstr("/qualified_id_join_index_dir/")))
+    EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+    EXPECT_CALL(*mock_filesystem,
+                DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
         .Times(0);
 
     TestIcingSearchEngine icing(
@@ -2702,8 +2726,8 @@
     index->set_last_added_document_id(original_last_added_doc_id + 1);
     Index::Editor editor =
         index->Edit(original_last_added_doc_id + 1, /*section_id=*/0,
-                    TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-    ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                    /*namespace_id=*/0);
+    ICING_ASSERT_OK(editor.BufferTerm("foo", TermMatchType::EXACT_ONLY));
     ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
   }
 
@@ -2733,9 +2757,9 @@
     EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
                                       EndsWith("/qualified_id_join_index_dir")))
         .Times(0);
-    EXPECT_CALL(
-        *mock_filesystem,
-        DeleteDirectoryRecursively(HasSubstr("/qualified_id_join_index_dir/")))
+    EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+    EXPECT_CALL(*mock_filesystem,
+                DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
         .Times(0);
 
     IcingSearchEngineOptions options = GetDefaultIcingOptions();
@@ -2948,8 +2972,8 @@
     index->set_last_added_document_id(original_last_added_doc_id + 1);
     Index::Editor editor =
         index->Edit(original_last_added_doc_id + 1, /*section_id=*/0,
-                    TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-    ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                    /*namespace_id=*/0);
+    ICING_ASSERT_OK(editor.BufferTerm("foo", TermMatchType::EXACT_ONLY));
     ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
   }
 
@@ -2979,9 +3003,9 @@
     EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
                                       EndsWith("/qualified_id_join_index_dir")))
         .Times(0);
-    EXPECT_CALL(
-        *mock_filesystem,
-        DeleteDirectoryRecursively(HasSubstr("/qualified_id_join_index_dir/")))
+    EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+    EXPECT_CALL(*mock_filesystem,
+                DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
         .Times(0);
 
     IcingSearchEngineOptions options = GetDefaultIcingOptions();
@@ -3146,18 +3170,17 @@
     // Add hits for document 0 and merge.
     ASSERT_THAT(index->last_added_document_id(), kInvalidDocumentId);
     index->set_last_added_document_id(0);
-    Index::Editor editor =
-        index->Edit(/*document_id=*/0, /*section_id=*/0,
-                    TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-    ICING_ASSERT_OK(editor.BufferTerm("foo"));
+    Index::Editor editor = index->Edit(/*document_id=*/0, /*section_id=*/0,
+                                       /*namespace_id=*/0);
+    ICING_ASSERT_OK(editor.BufferTerm("foo", TermMatchType::EXACT_ONLY));
     ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
     ICING_ASSERT_OK(index->Merge());
 
     // Add hits for document 1 and don't merge.
     index->set_last_added_document_id(1);
     editor = index->Edit(/*document_id=*/1, /*section_id=*/0,
-                         TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-    ICING_ASSERT_OK(editor.BufferTerm("bar"));
+                         /*namespace_id=*/0);
+    ICING_ASSERT_OK(editor.BufferTerm("bar", TermMatchType::EXACT_ONLY));
     ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
   }
 
@@ -3187,9 +3210,9 @@
     EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
                                       EndsWith("/qualified_id_join_index_dir")))
         .Times(0);
-    EXPECT_CALL(
-        *mock_filesystem,
-        DeleteDirectoryRecursively(HasSubstr("/qualified_id_join_index_dir/")))
+    EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+    EXPECT_CALL(*mock_filesystem,
+                DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
         .Times(AtLeast(1));
 
     TestIcingSearchEngine icing(
@@ -3344,16 +3367,16 @@
     index->set_last_added_document_id(original_last_added_doc_id + 1);
     Index::Editor editor =
         index->Edit(original_last_added_doc_id + 1, /*section_id=*/0,
-                    TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-    ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                    /*namespace_id=*/0);
+    ICING_ASSERT_OK(editor.BufferTerm("foo", TermMatchType::EXACT_ONLY));
     ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
     ICING_ASSERT_OK(index->Merge());
 
     // Add hits for document 5 and don't merge.
     index->set_last_added_document_id(original_last_added_doc_id + 2);
     editor = index->Edit(original_last_added_doc_id + 2, /*section_id=*/0,
-                         TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-    ICING_ASSERT_OK(editor.BufferTerm("bar"));
+                         /*namespace_id=*/0);
+    ICING_ASSERT_OK(editor.BufferTerm("bar", TermMatchType::EXACT_ONLY));
     ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
   }
 
@@ -3384,9 +3407,9 @@
     EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
                                       EndsWith("/qualified_id_join_index_dir")))
         .Times(0);
-    EXPECT_CALL(
-        *mock_filesystem,
-        DeleteDirectoryRecursively(HasSubstr("/qualified_id_join_index_dir/")))
+    EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+    EXPECT_CALL(*mock_filesystem,
+                DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
         .Times(0);
 
     TestIcingSearchEngine icing(
@@ -3579,9 +3602,9 @@
     EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
                                       EndsWith("/qualified_id_join_index_dir")))
         .Times(0);
-    EXPECT_CALL(
-        *mock_filesystem,
-        DeleteDirectoryRecursively(HasSubstr("/qualified_id_join_index_dir/")))
+    EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+    EXPECT_CALL(*mock_filesystem,
+                DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
         .Times(AtLeast(1));
 
     TestIcingSearchEngine icing(
@@ -3758,9 +3781,9 @@
     EXPECT_CALL(*mock_filesystem, DeleteDirectoryRecursively(
                                       EndsWith("/qualified_id_join_index_dir")))
         .Times(0);
-    EXPECT_CALL(
-        *mock_filesystem,
-        DeleteDirectoryRecursively(HasSubstr("/qualified_id_join_index_dir/")))
+    EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+    EXPECT_CALL(*mock_filesystem,
+                DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
         .Times(0);
 
     TestIcingSearchEngine icing(
@@ -3913,18 +3936,15 @@
     Filesystem filesystem;
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<QualifiedIdJoinIndex> qualified_id_join_index,
-        QualifiedIdJoinIndexImplV2::Create(filesystem,
-                                           GetQualifiedIdJoinIndexDir(),
-                                           /*pre_mapping_fbv=*/false));
+        QualifiedIdJoinIndexImplV3::Create(
+            filesystem, GetQualifiedIdJoinIndexDir(), *feature_flags_));
     // Add data for document 0.
     ASSERT_THAT(qualified_id_join_index->last_added_document_id(),
                 kInvalidDocumentId);
     qualified_id_join_index->set_last_added_document_id(0);
     ICING_ASSERT_OK(qualified_id_join_index->Put(
-        /*schema_type_id=*/0, /*joinable_property_id=*/0, /*document_id=*/0,
-        /*ref_namespace_fingerprint_ids=*/
-        {NamespaceFingerprintIdentifier(/*namespace_id=*/0,
-                                        /*target_str=*/"uri")}));
+        DocumentJoinIdPair(/*document_id=*/0, /*joinable_property_id=*/0),
+        /*parent_document_ids=*/std::vector<DocumentId>{0}));
   }
 
   // 3. Create the index again. This should trigger index restoration.
@@ -3953,9 +3973,9 @@
     // Clear() should be called to truncate qualified id join index and thus
     // underlying storage sub directory (path_expr =
     // "*/qualified_id_join_index_dir/*") should be discarded.
-    EXPECT_CALL(
-        *mock_filesystem,
-        DeleteDirectoryRecursively(HasSubstr("/qualified_id_join_index_dir/")))
+    EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+    EXPECT_CALL(*mock_filesystem,
+                DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
         .Times(AtLeast(1));
 
     TestIcingSearchEngine icing(
@@ -3985,14 +4005,11 @@
     Filesystem filesystem;
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<QualifiedIdJoinIndex> qualified_id_join_index,
-        QualifiedIdJoinIndexImplV2::Create(filesystem,
-                                           GetQualifiedIdJoinIndexDir(),
-                                           /*pre_mapping_fbv=*/false));
-    ICING_ASSERT_OK_AND_ASSIGN(
-        auto iterator, qualified_id_join_index->GetIterator(
-                           /*schema_type_id=*/0, /*joinable_property_id=*/0));
-    EXPECT_THAT(iterator->Advance(),
-                StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
+        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),
+                IsOkAndHolds(IsEmpty()));
   }
 }
 
@@ -4087,20 +4104,16 @@
     Filesystem filesystem;
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<QualifiedIdJoinIndex> qualified_id_join_index,
-        QualifiedIdJoinIndexImplV2::Create(filesystem,
-                                           GetQualifiedIdJoinIndexDir(),
-                                           /*pre_mapping_fbv=*/false));
+        QualifiedIdJoinIndexImplV3::Create(
+            filesystem, GetQualifiedIdJoinIndexDir(), *feature_flags_));
     // Add data for document 4.
     DocumentId original_last_added_doc_id =
         qualified_id_join_index->last_added_document_id();
     qualified_id_join_index->set_last_added_document_id(
         original_last_added_doc_id + 1);
     ICING_ASSERT_OK(qualified_id_join_index->Put(
-        /*schema_type_id=*/1, /*joinable_property_id=*/0,
-        /*document_id=*/original_last_added_doc_id + 1,
-        /*ref_namespace_fingerprint_ids=*/
-        {NamespaceFingerprintIdentifier(/*namespace_id=*/0,
-                                        /*target_str=*/"person")}));
+        DocumentJoinIdPair(/*document_id=*/4, /*joinable_property_id=*/0),
+        /*parent_document_ids=*/std::vector<DocumentId>{0}));
   }
 
   // 3. Create the index again. This should trigger index restoration.
@@ -4129,9 +4142,9 @@
     // Clear() should be called to truncate qualified id join index and thus
     // underlying storage sub directory (path_expr =
     // "*/qualified_id_join_index_dir/*") should be discarded.
-    EXPECT_CALL(
-        *mock_filesystem,
-        DeleteDirectoryRecursively(HasSubstr("/qualified_id_join_index_dir/")))
+    EXPECT_CALL(*mock_filesystem, DeleteFile(_)).WillRepeatedly(DoDefault());
+    EXPECT_CALL(*mock_filesystem,
+                DeleteFile(HasSubstr("/qualified_id_join_index_dir/")))
         .Times(AtLeast(1));
 
     TestIcingSearchEngine icing(
@@ -4509,9 +4522,7 @@
     // Document store rewinds to previous checkpoint and all derived files were
     // regenerated.
     // - Last stored doc id will be consistent with last added document ids in
-    //   term/integer indices, so there will be no index restoration.
-    // - Qualified id join index depends on document store derived files and
-    //   since they were regenerated, we should rebuild qualified id join index.
+    //   term/integer/join indices, so there will be no index restoration.
     EXPECT_THAT(
         initialize_result_proto.initialize_stats().index_restoration_cause(),
         Eq(InitializeStatsProto::NONE));
@@ -4520,10 +4531,10 @@
                 Eq(InitializeStatsProto::NONE));
     EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .qualified_id_join_index_restoration_cause(),
-                Eq(InitializeStatsProto::DEPENDENCIES_CHANGED));
+                Eq(InitializeStatsProto::NONE));
     EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .index_restoration_latency_ms(),
-                Eq(10));
+                Eq(0));
     EXPECT_THAT(initialize_result_proto.initialize_stats()
                     .schema_store_recovery_cause(),
                 Eq(InitializeStatsProto::NONE));
@@ -4904,7 +4915,8 @@
     FakeClock fake_clock;
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(filesystem(), GetSchemaDir(), &fake_clock));
+        SchemaStore::Create(filesystem(), GetSchemaDir(), &fake_clock,
+                            feature_flags_.get()));
     ICING_EXPECT_OK(schema_store->SetSchema(
         new_schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -5175,12 +5187,11 @@
   auto mock_filesystem = std::make_unique<MockFilesystem>();
   EXPECT_CALL(*mock_filesystem, PRead(A<const char*>(), _, _, _))
       .WillRepeatedly(DoDefault());
-  // This fails QualifiedIdJoinIndexImplV2::Create() once.
-  EXPECT_CALL(
-      *mock_filesystem,
-      PRead(Matcher<const char*>(Eq(qualified_id_join_index_metadata_file)), _,
-            _, _))
-      .WillOnce(Return(false))
+  // This fails QualifiedIdJoinIndexImplV3::Create() once.
+  EXPECT_CALL(*mock_filesystem, OpenForWrite(_)).WillRepeatedly(DoDefault());
+  EXPECT_CALL(*mock_filesystem, OpenForWrite(Matcher<const char*>(
+                                    Eq(qualified_id_join_index_metadata_file))))
+      .WillOnce(Return(-1))
       .WillRepeatedly(DoDefault());
 
   auto fake_clock = std::make_unique<FakeClock>();
@@ -5275,10 +5286,10 @@
               Eq(InitializeStatsProto::NONE));
   EXPECT_THAT(initialize_result_proto.initialize_stats()
                   .qualified_id_join_index_restoration_cause(),
-              Eq(InitializeStatsProto::DEPENDENCIES_CHANGED));
+              Eq(InitializeStatsProto::NONE));
   EXPECT_THAT(
       initialize_result_proto.initialize_stats().index_restoration_latency_ms(),
-      Eq(10));
+      Eq(0));
   EXPECT_THAT(
       initialize_result_proto.initialize_stats().schema_store_recovery_cause(),
       Eq(InitializeStatsProto::NONE));
@@ -5427,6 +5438,11 @@
                                         .SetName("senderQualifiedId")
                                         .SetDataTypeJoinableString(
                                             JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("embedding")
+                                        .SetDataTypeVector(
+                                            EMBEDDING_INDEXING_LINEAR_SEARCH)
                                         .SetCardinality(CARDINALITY_REQUIRED)))
           .Build();
 
@@ -5451,6 +5467,8 @@
           .AddStringProperty("body", "correct message")
           .AddInt64Property("indexableInteger", 123)
           .AddStringProperty("senderQualifiedId", "namespace#person/1")
+          .AddVectorProperty(
+              "embedding", CreateVector("my_model", {0.1, 0.2, 0.3, 0.4, 0.5}))
           .SetCreationTimestampMs(kDefaultCreationTimestampMs)
           .Build();
 
@@ -5480,24 +5498,26 @@
     FakeClock fake_clock;
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(filesystem(), GetSchemaDir(), &fake_clock));
+        SchemaStore::Create(filesystem(), GetSchemaDir(), &fake_clock,
+                            feature_flags_.get()));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         BlobStore blob_store,
         BlobStore::Create(filesystem(), GetBlobDir(), &fake_clock,
-                          /*orphan_blob_time_to_live_ms=*/0));
+                          /*orphan_blob_time_to_live_ms=*/0,
+                          PortableFileBackedProtoLog<
+                              BlobInfoProto>::kDefaultCompressionLevel));
 
     // Put message into DocumentStore
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
         DocumentStore::Create(filesystem(), GetDocumentDir(), &fake_clock,
-                              schema_store.get(),
+                              schema_store.get(), feature_flags_.get(),
                               /*force_recovery_and_revalidate_documents=*/false,
-                              /*namespace_id_fingerprint=*/true,
                               /*pre_mapping_fbv=*/false,
                               /*use_persistent_hash_map=*/true,
                               PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDeflateCompressionLevel,
+                                  DocumentWrapper>::kDefaultCompressionLevel,
                               /*initialize_stats=*/nullptr));
     std::unique_ptr<DocumentStore> document_store =
         std::move(create_result.document_store);
@@ -5521,9 +5541,13 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<QualifiedIdJoinIndex> qualified_id_join_index,
-        QualifiedIdJoinIndexImplV2::Create(*filesystem(),
-                                           GetQualifiedIdJoinIndexDir(),
-                                           /*pre_mapping_fbv=*/false));
+        QualifiedIdJoinIndexImplV3::Create(
+            *filesystem(), GetQualifiedIdJoinIndexDir(), *feature_flags_));
+
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<EmbeddingIndex> embedding_index,
+        EmbeddingIndex::Create(filesystem(), GetEmbeddingIndexDir(),
+                               &fake_clock, feature_flags_.get()));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<TermIndexingHandler> term_indexing_handler,
@@ -5539,10 +5563,15 @@
             qualified_id_join_indexing_handler,
         QualifiedIdJoinIndexingHandler::Create(
             &fake_clock, document_store.get(), qualified_id_join_index.get()));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<EmbeddingIndexingHandler> embedding_indexing_handler,
+        EmbeddingIndexingHandler::Create(&fake_clock, embedding_index.get(),
+                                         /*enable_embedding_index=*/true));
     std::vector<std::unique_ptr<DataIndexingHandler>> handlers;
     handlers.push_back(std::move(term_indexing_handler));
     handlers.push_back(std::move(integer_section_indexing_handler));
     handlers.push_back(std::move(qualified_id_join_indexing_handler));
+    handlers.push_back(std::move(embedding_indexing_handler));
     IndexProcessor index_processor(std::move(handlers), &fake_clock);
 
     DocumentProto incorrect_message =
@@ -5552,13 +5581,17 @@
             .AddStringProperty("body", "wrong message")
             .AddInt64Property("indexableInteger", 456)
             .AddStringProperty("senderQualifiedId", "namespace#person/2")
+            .AddVectorProperty(
+                "embedding",
+                CreateVector("my_model", {-0.1, -0.2, -0.3, -0.4, -0.5}))
             .SetCreationTimestampMs(kDefaultCreationTimestampMs)
             .Build();
     ICING_ASSERT_OK_AND_ASSIGN(
         TokenizedDocument tokenized_document,
         TokenizedDocument::Create(schema_store.get(), lang_segmenter_.get(),
                                   std::move(incorrect_message)));
-    ICING_ASSERT_OK(index_processor.IndexDocument(tokenized_document, doc_id));
+    ICING_ASSERT_OK(index_processor.IndexDocument(tokenized_document, doc_id,
+                                                  put_result.old_document_id));
 
     // Rewrite existing data's version files
     ICING_ASSERT_OK(
@@ -5604,6 +5637,7 @@
   //  - term index
   //  - numeric index
   //  - qualified id join index
+  //  - embedding index
   InitializeStatsProto::RecoveryCause expected_recovery_cause =
       GetParam().existing_version_info.version != version_util::kVersion
           ? InitializeStatsProto::VERSION_CHANGED
@@ -5622,6 +5656,9 @@
   EXPECT_THAT(initialize_result.initialize_stats()
                   .qualified_id_join_index_restoration_cause(),
               Eq(expected_recovery_cause));
+  EXPECT_THAT(
+      initialize_result.initialize_stats().embedding_index_restoration_cause(),
+      Eq(expected_recovery_cause));
 
   // Manually check version file
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -5698,6 +5735,22 @@
       search_spec3, ScoringSpecProto::default_instance(), result_spec3);
   EXPECT_THAT(search_result_proto3, EqualsSearchResultIgnoreStatsAndScores(
                                         expected_join_search_result_proto));
+
+  // Verify embedding search
+  SearchSpecProto search_spec4;
+  search_spec4.set_query("semanticSearch(getEmbeddingParameter(0), 0)");
+  *search_spec4.add_embedding_query_vectors() =
+      CreateVector("my_model", {1, 1, 1, 1, 1});
+  search_spec4.set_embedding_query_metric_type(
+      SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT);
+  search_spec4.add_enabled_features(
+      std::string(kListFilterQueryLanguageFeature));
+
+  SearchResultProto search_result_proto4 =
+      icing.Search(search_spec4, ScoringSpecProto::default_instance(),
+                   ResultSpecProto::default_instance());
+  EXPECT_THAT(search_result_proto4, EqualsSearchResultIgnoreStatsAndScores(
+                                        expected_search_result_proto));
 }
 
 INSTANTIATE_TEST_SUITE_P(
@@ -5913,6 +5966,831 @@
     testing::Values(std::make_tuple(false, false), std::make_tuple(false, true),
                     std::make_tuple(true, false), std::make_tuple(true, true)));
 
+class IcingSearchEngineInitializationChangeEmbeddingFlagTest
+    : public IcingSearchEngineInitializationTest,
+      public ::testing::WithParamInterface<std::vector<bool>> {};
+TEST_P(IcingSearchEngineInitializationChangeEmbeddingFlagTest,
+       ChangeEnableEmbeddingIndexFlagTest) {
+  std::vector<bool> enable_embedding_index_flags = GetParam();
+
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Message").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("embedding")
+                  .SetDataTypeVector(EMBEDDING_INDEXING_LINEAR_SEARCH)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+
+  // Create a document with an embedding.
+  DocumentProto document =
+      DocumentBuilder()
+          .SetKey("icing", "uri")
+          .SetSchema("Message")
+          .SetCreationTimestampMs(1)
+          .AddVectorProperty(
+              "embedding", CreateVector("my_model", {0.1, 0.2, 0.3, 0.4, 0.5}))
+          .Build();
+
+  // Create icing with a document that has an embedding.
+  {
+    IcingSearchEngineOptions options = GetDefaultIcingOptions();
+    options.set_enable_embedding_index(enable_embedding_index_flags[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(document).status(), ProtoIsOk());
+  }
+
+  // Create icing multiple times with different enable_embedding_index flags.
+  for (int i = 1; i < enable_embedding_index_flags.size(); ++i) {
+    bool flag_changed =
+        enable_embedding_index_flags[i] != enable_embedding_index_flags[i - 1];
+
+    // Ensure that the embedding index is rebuilt if the flag 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(flag_changed ? 1 : 0);
+
+    IcingSearchEngineOptions options = GetDefaultIcingOptions();
+    options.set_enable_embedding_index(enable_embedding_index_flags[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 the flag is changed.
+    EXPECT_THAT(initialize_result.initialize_stats()
+                    .embedding_index_restoration_cause(),
+                Eq(flag_changed ? InitializeStatsProto::FEATURE_FLAG_CHANGED
+                                : 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 a embedding query that should match the document if the embedding
+    // index is enabled.
+    SearchSpecProto search_spec;
+    search_spec.set_query("semanticSearch(getEmbeddingParameter(0), 0)");
+    *search_spec.add_embedding_query_vectors() =
+        CreateVector("my_model", {1, 1, 1, 1, 1});
+    search_spec.set_embedding_query_metric_type(
+        SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT);
+    search_spec.add_enabled_features(
+        std::string(kListFilterQueryLanguageFeature));
+
+    SearchResultProto results =
+        icing.Search(search_spec, GetDefaultScoringSpec(),
+                     ResultSpecProto::default_instance());
+    EXPECT_THAT(results.status(), ProtoIsOk());
+    // The document should be returned if the embedding index is enabled.
+    if (enable_embedding_index_flags[i]) {
+      EXPECT_THAT(results.results(), SizeIs(1));
+      EXPECT_THAT(results.results(0).document(), EqualsProto(document));
+    } else {
+      EXPECT_THAT(results.results(), IsEmpty());
+    }
+  }
+}
+
+TEST_P(IcingSearchEngineInitializationChangeEmbeddingFlagTest,
+       ChangeEnableEmbeddingQuantizationFlagTest) {
+  std::vector<bool> enable_embedding_quantization_flags = GetParam();
+
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Message")
+                  .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)))
+          .Build();
+
+  // If quantization is enabled, this vector will be quantized to {0, 1, 255}.
+  PropertyProto::VectorProto vector = CreateVector("my_model", {0, 1.45, 255});
+  // Create two documents with different quantization types. If quantization is
+  // enabled, then only document2's embedding will be quantized. Otherwise, both
+  // will be unquantized.
+  DocumentProto document1 =
+      DocumentBuilder()
+          .SetKey("icing", "uri1")
+          .SetSchema("Message")
+          .SetCreationTimestampMs(1)
+          .AddVectorProperty("embeddingUnquantized", vector)
+          .Build();
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("icing", "uri2")
+                                .SetSchema("Message")
+                                .SetCreationTimestampMs(1)
+                                .AddVectorProperty("embeddingQuantized", vector)
+                                .Build();
+
+  // Create icing with a document that has an embedding.
+  {
+    IcingSearchEngineOptions options = GetDefaultIcingOptions();
+    options.set_enable_embedding_index(true);
+    options.set_enable_embedding_quantization(
+        enable_embedding_quantization_flags[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(document1).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(document2).status(), ProtoIsOk());
+  }
+
+  // Create icing multiple times with different enable_embedding_index flags.
+  for (int i = 1; i < enable_embedding_quantization_flags.size(); ++i) {
+    bool flag_changed = enable_embedding_quantization_flags[i] !=
+                        enable_embedding_quantization_flags[i - 1];
+
+    // Ensure that the embedding index is rebuilt if the flag 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(flag_changed ? 1 : 0);
+
+    IcingSearchEngineOptions options = GetDefaultIcingOptions();
+    options.set_enable_embedding_index(true);
+    options.set_enable_embedding_quantization(
+        enable_embedding_quantization_flags[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 the flag is changed.
+    EXPECT_THAT(initialize_result.initialize_stats()
+                    .embedding_index_restoration_cause(),
+                Eq(flag_changed ? InitializeStatsProto::FEATURE_FLAG_CHANGED
+                                : 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. We will
+    // check quantization by checking the scores.
+    //
+    // This query will assign a dot product score that is equal to the sum of
+    // all dimensions of the matched embedding. As a result, if quantization is
+    // enabled, the score will be 0 + 1 + 255 = 256. Otherwise, the score will
+    // be 0 + 1.45 + 255 = 256.45.
+    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.
+    scoring_spec.add_additional_advanced_scoring_expressions(
+        "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))");
+
+    SearchResultProto results = icing.Search(
+        search_spec, scoring_spec, ResultSpecProto::default_instance());
+    EXPECT_THAT(results.status(), ProtoIsOk());
+    EXPECT_THAT(results.results(), SizeIs(2));
+
+    constexpr float eps = 0.0001f;
+    constexpr float unquantized_score = 256.45f;
+    constexpr float quantized_score = 256.0f;
+    EXPECT_THAT(results.results(0).document(), EqualsProto(document2));
+    if (enable_embedding_quantization_flags[i]) {
+      // When quantization is enabled, only the embedding that is configured
+      // with quantization is quantized. So only document2's embedding is
+      // quantized.
+      EXPECT_NEAR(results.results(0).additional_scores(0), quantized_score,
+                  eps);
+    } else {
+      // When quantization is disabled, all embeddings are unquantized.
+      EXPECT_NEAR(results.results(0).additional_scores(0), unquantized_score,
+                  eps);
+    }
+    // document1's embedding is always unquantized, since it is not configured
+    // to be quantized.
+    EXPECT_THAT(results.results(1).document(), EqualsProto(document1));
+    EXPECT_NEAR(results.results(1).additional_scores(0), unquantized_score,
+                eps);
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    IcingSearchEngineInitializationChangeEmbeddingFlagTest,
+    IcingSearchEngineInitializationChangeEmbeddingFlagTest,
+    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 IcingSearchEngineInitializationChangeEnableScorablePropertiesFlagTest
+    : public IcingSearchEngineInitializationTest,
+      public ::testing::WithParamInterface<std::vector<bool>> {};
+TEST_P(IcingSearchEngineInitializationChangeEnableScorablePropertiesFlagTest,
+       ChangeEnableScorablePropertiesFlagTest) {
+  std::vector<bool> enable_scorable_properties_flags = GetParam();
+
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Message")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("body")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("score")
+                                        .SetDataTypeInt64(NUMERIC_MATCH_UNKNOWN)
+                                        .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                        .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+
+  // Create a document with an embedding.
+  int scorable_prop_value = 10;
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("icing", "uri")
+                               .SetSchema("Message")
+                               .SetCreationTimestampMs(1)
+                               .AddStringProperty("body", "foo bar")
+                               .AddInt64Property("score", scorable_prop_value)
+                               .Build();
+
+  // Create icing with a document that has a scorable property.
+  {
+    IcingSearchEngineOptions options = GetDefaultIcingOptions();
+    options.set_enable_scorable_properties(
+        enable_scorable_properties_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(document).status(), ProtoIsOk());
+  }
+
+  // Create icing multiple times with different enable_scorable_properties
+  // flags.
+  for (int i = 1; i < enable_scorable_properties_flags.size(); ++i) {
+    bool flag_changed = enable_scorable_properties_flags[i] !=
+                        enable_scorable_properties_flags[i - 1];
+
+    // Ensure that the document store derived files are rebuilt if the flag is
+    // changed.
+    IcingSearchEngineOptions options = GetDefaultIcingOptions();
+    options.set_enable_scorable_properties(enable_scorable_properties_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());
+
+    // Document store recovery cause should be FEATURE_FLAG_CHANGED if the flag
+    // is changed.
+    EXPECT_THAT(
+        initialize_result.initialize_stats().document_store_recovery_cause(),
+        Eq(flag_changed ? InitializeStatsProto::FEATURE_FLAG_CHANGED
+                        : InitializeStatsProto::NONE));
+
+    // Schema store and all indices should be unaffected.
+    EXPECT_THAT(
+        initialize_result.initialize_stats().schema_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));
+
+    // Write normal query that should retrieve the document.
+    SearchSpecProto search_spec;
+    search_spec.set_query("bar");
+    search_spec.set_term_match_type(TermMatchType::EXACT_ONLY);
+    search_spec.add_enabled_features(
+        std::string(kListFilterQueryLanguageFeature));
+
+    ScoringSpecProto scoring_spec;
+    scoring_spec.add_scoring_feature_types_enabled(
+        ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+    scoring_spec.set_rank_by(
+        ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+    scoring_spec.set_advanced_scoring_expression("this.creationTimestamp()");
+    SearchResultProto 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(document));
+    EXPECT_THAT(results.results(0).score(), Eq(1));
+
+    // Now write a query that tries to access the scorable property.
+    scoring_spec.set_advanced_scoring_expression(
+        "this.creationTimestamp() + sum(getScorableProperty(\"Message\", "
+        "\"score\"))");
+    SchemaTypeAliasMapProto* alias_map_proto =
+        scoring_spec.add_schema_type_alias_map_protos();
+    alias_map_proto->set_alias_schema_type("Message");
+    alias_map_proto->mutable_schema_types()->Add("Message");
+
+    results = icing.Search(search_spec, scoring_spec,
+                           ResultSpecProto::default_instance());
+    if (options.enable_scorable_properties()) {
+      EXPECT_THAT(results.status(), ProtoIsOk());
+      EXPECT_THAT(results.results(), SizeIs(1));
+      EXPECT_THAT(results.results(0).document(), EqualsProto(document));
+      EXPECT_THAT(results.results(0).score(), Eq(1 + scorable_prop_value));
+    } else {
+      EXPECT_THAT(results.status(),
+                  ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
+    }
+
+    // Reindex the doc with a new scorable property value, regardless of whether
+    // scorable properties are enabled. This way we can confirm that Initialize
+    // is actually rebuilding the scorable property cache with new values.
+    scorable_prop_value += 10;
+    document = DocumentBuilder(document)
+                   .ClearProperties()
+                   .AddStringProperty("body", "foo bar")
+                   .AddInt64Property("score", scorable_prop_value)
+                   .Build();
+    ASSERT_THAT(icing.Put(document).status(), ProtoIsOk());
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    IcingSearchEngineInitializationChangeEnableScorablePropertiesFlagTest,
+    IcingSearchEngineInitializationChangeEnableScorablePropertiesFlagTest,
+    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 IcingSearchEngineInitializationSchemaDatabaseMigrationTest
+    : public IcingSearchEngineInitializationTest,
+      public ::testing::WithParamInterface<std::tuple<int32_t, bool, bool>> {};
+
+TEST_P(IcingSearchEngineInitializationSchemaDatabaseMigrationTest,
+       InitializeWithSchemaDatabaseMigration) {
+  int32_t existing_version = std::get<0>(GetParam());
+  bool previous_version_has_schema_database_enabled = std::get<1>(GetParam());
+  bool enable_schema_database = std::get<2>(GetParam());
+
+  IcingSearchEngineVersionProto previous_version_proto;
+  previous_version_proto.set_version(existing_version);
+  previous_version_proto.set_max_version(existing_version);
+  if (previous_version_has_schema_database_enabled) {
+    previous_version_proto.add_enabled_features()->set_feature_type(
+        IcingSearchEngineFeatureInfoProto::FEATURE_SCHEMA_DATABASE);
+  }
+
+  SchemaTypeConfigProto db1_email_type =
+      SchemaTypeConfigBuilder()
+          .SetType("db1/email")
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("db1Subject")
+                           .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                           .SetCardinality(CARDINALITY_OPTIONAL))
+          .Build();
+  SchemaTypeConfigProto db2_email_type =
+      SchemaTypeConfigBuilder()
+          .SetType("db2/email")
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("db2Subject")
+                           .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                           .SetCardinality(CARDINALITY_OPTIONAL))
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("db2Id")
+                           .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                           .SetCardinality(CARDINALITY_OPTIONAL))
+          .Build();
+
+  if (previous_version_has_schema_database_enabled) {
+    // Populate the database field for the db1/email type.
+    db1_email_type =
+        SchemaTypeConfigBuilder(db1_email_type).SetDatabase("db1").Build();
+    if (existing_version >= version_util::kSchemaDatabaseVersion) {
+      // Populate the database field for the db2/email type only if previous
+      // version is a post schema-database version.
+      db2_email_type =
+          SchemaTypeConfigBuilder(db2_email_type).SetDatabase("db2").Build();
+    }
+    // Otherwise, the database field is not populated for db2/email type. This
+    // is to simulate the following situation:
+    // 1. Icing is initialized on a version>kSchemaDatabaseVersion with schema
+    //    database enabled, and db1/email is set with the database field
+    //    populated.
+    // 2. Icing gets rolled back to pre-schema database version, db2/email is
+    //    set during this time so the database field is not populated.
+  }
+  SchemaProto previous_version_schema =
+      SchemaBuilder().AddType(db1_email_type).AddType(db2_email_type).Build();
+
+  DocumentProto db1_email_doc =
+      DocumentBuilder()
+          .SetKey("namespace", "uri1")
+          .SetSchema("db1/email")
+          .AddStringProperty("db1Subject", "subject")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto db2_email_doc =
+      DocumentBuilder()
+          .SetKey("namespace", "uri3")
+          .SetSchema("db2/email")
+          .AddStringProperty("db2Subject", "subject")
+          .AddInt64Property("db2Id", 123)
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+
+  {  // Initialize IcingSearchEngine
+    IcingSearchEngineOptions options = GetDefaultIcingOptions();
+    options.set_enable_schema_database(
+        previous_version_has_schema_database_enabled);
+    TestIcingSearchEngine icing(options, std::make_unique<Filesystem>(),
+                                std::make_unique<IcingFilesystem>(),
+                                std::make_unique<FakeClock>(),
+                                GetTestJniCache());
+    EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+    // 1. Set schema.
+    if (options.enable_schema_database()) {
+      // Need to set schemas with a single database field at a time.
+      ASSERT_THAT(
+          icing.SetSchema(SchemaBuilder().AddType(db1_email_type).Build())
+              .status(),
+          ProtoIsOk());
+      ASSERT_THAT(
+          icing.SetSchema(SchemaBuilder().AddType(db2_email_type).Build())
+              .status(),
+          ProtoIsOk());
+    } else {
+      ASSERT_THAT(icing.SetSchema(previous_version_schema).status(),
+                  ProtoIsOk());
+    }
+    // 2. Put two documents
+    ASSERT_THAT(icing.Put(db1_email_doc).status(), ProtoIsOk());
+    ASSERT_THAT(icing.Put(db2_email_doc).status(), ProtoIsOk());
+    // 3. Rewrite version files
+    //    - Only need to rewrite v1 version file to write an older version
+    //      number.
+    //    - FeatureInfo rewritting (v2 version file) is not needed as it should
+    //      be handled by IcingSearchEngine.
+    ICING_ASSERT_OK(version_util::WriteV1Version(
+        *filesystem(), GetVersionFileDir(),
+        version_util::VersionInfo(existing_version, existing_version)));
+  }  // This should shut down IcingSearchEngine and persist anything it needs to
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_schema_database(enable_schema_database);
+
+  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());
+
+  SearchResultProto db1_email_search_result_proto;
+  db1_email_search_result_proto.mutable_status()->set_code(StatusProto::OK);
+  *db1_email_search_result_proto.mutable_results()->Add()->mutable_document() =
+      db1_email_doc;
+
+  SearchResultProto db2_email_search_result_proto;
+  db2_email_search_result_proto.mutable_status()->set_code(StatusProto::OK);
+  *db2_email_search_result_proto.mutable_results()->Add()->mutable_document() =
+      db2_email_doc;
+
+  SearchResultProto all_email_search_result_proto;
+  all_email_search_result_proto.mutable_status()->set_code(StatusProto::OK);
+  *all_email_search_result_proto.mutable_results()->Add()->mutable_document() =
+      db2_email_doc;
+  *all_email_search_result_proto.mutable_results()->Add()->mutable_document() =
+      db1_email_doc;
+
+  // Verify term search
+  SearchSpecProto search_spec1;
+  search_spec1.set_query("db1Subject:subject");
+  search_spec1.set_term_match_type(TermMatchType::EXACT_ONLY);
+  SearchResultProto search_result_proto1 =
+      icing.Search(search_spec1, GetDefaultScoringSpec(),
+                   ResultSpecProto::default_instance());
+  EXPECT_THAT(search_result_proto1, EqualsSearchResultIgnoreStatsAndScores(
+                                        db1_email_search_result_proto));
+
+  SearchSpecProto search_spec2;
+  search_spec2.set_query("subject");
+  search_spec2.set_term_match_type(TermMatchType::EXACT_ONLY);
+  SearchResultProto search_result_google::protobuf =
+      icing.Search(search_spec2, GetDefaultScoringSpec(),
+                   ResultSpecProto::default_instance());
+  EXPECT_THAT(search_result_google::protobuf, EqualsSearchResultIgnoreStatsAndScores(
+                                        all_email_search_result_proto));
+
+  // Verify numeric (integer) search
+  SearchSpecProto search_spec3;
+  search_spec3.set_query("db2Id == 123");
+  search_spec3.add_enabled_features(std::string(kNumericSearchFeature));
+
+  SearchResultProto search_result_proto3 =
+      icing.Search(search_spec3, ScoringSpecProto::default_instance(),
+                   ResultSpecProto::default_instance());
+  EXPECT_THAT(search_result_proto3, EqualsSearchResultIgnoreStatsAndScores(
+                                        db2_email_search_result_proto));
+
+  // Verify GetSchema
+  if (enable_schema_database) {
+    SchemaTypeConfigProto db1_email_type_with_db =
+        SchemaTypeConfigBuilder(db1_email_type).SetDatabase("db1").Build();
+    SchemaTypeConfigProto db2_email_type_with_db =
+        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)
+                                                .Build();
+    SchemaProto db1_schema =
+        SchemaBuilder().AddType(db1_email_type_with_db).Build();
+    SchemaProto db2_schema =
+        SchemaBuilder().AddType(db2_email_type_with_db).Build();
+
+    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_with_database;
+    EXPECT_THAT(icing.GetSchema(),
+                EqualsProto(expected_get_schema_result_proto_full));
+
+    GetSchemaResultProto expected_get_schema_result_proto_db1;
+    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"),
+                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"),
+                EqualsProto(expected_get_schema_result_proto_db2));
+  } else {
+    GetSchemaResultProto expected_get_schema_result_proto;
+    expected_get_schema_result_proto.mutable_status()->set_code(
+        StatusProto::OK);
+    *expected_get_schema_result_proto.mutable_schema() =
+        previous_version_schema;
+    EXPECT_THAT(icing.GetSchema(),
+                EqualsProto(expected_get_schema_result_proto));
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    IcingSearchEngineInitializationSchemaDatabaseMigrationTest,
+    IcingSearchEngineInitializationSchemaDatabaseMigrationTest,
+    testing::Values(
+        std::make_tuple(
+            /*previous_version=*/version_util::kSchemaDatabaseVersion - 1,
+            /*prev_version_schema_database_enabled=*/false,
+            /*enable_schema_database=*/false),
+        std::make_tuple(
+            /*previous_version=*/version_util::kSchemaDatabaseVersion - 1,
+            /*prev_version_schema_database_enabled=*/false,
+            /*enable_schema_database=*/true),
+        // The next two cases simulate the following scenario:
+        // 1. Icing is initialized on a version>kSchemaDatabaseVersion for
+        //    sometime, and schemas are set with the database field populated.
+        // 2. Icing gets rolled back to pre-schema database version, so new
+        //    schema types no longer populate the database field.
+        // 3. Icing gets rolled forward to post-schema database version again,
+        //    and we should verify that database migration happens correctly.
+        std::make_tuple(
+            /*previous_version=*/version_util::kSchemaDatabaseVersion - 1,
+            /*prev_version_schema_database_enabled=*/true,
+            /*enable_schema_database=*/false),
+        std::make_tuple(
+            /*previous_version=*/version_util::kSchemaDatabaseVersion - 1,
+            /*prev_version_schema_database_enabled=*/true,
+            /*enable_schema_database=*/true),
+        std::make_tuple(
+            /*previous_version=*/version_util::kSchemaDatabaseVersion,
+            /*prev_version_schema_database_enabled=*/false,
+            /*enable_schema_database=*/false),
+        std::make_tuple(
+            /*previous_version=*/version_util::kSchemaDatabaseVersion,
+            /*prev_version_schema_database_enabled=*/false,
+            /*enable_schema_database=*/true),
+        std::make_tuple(
+            /*previous_version=*/version_util::kSchemaDatabaseVersion,
+            /*prev_version_schema_database_enabled=*/true,
+            /*enable_schema_database=*/false),
+        std::make_tuple(
+            /*previous_version=*/version_util::kSchemaDatabaseVersion,
+            /*prev_version_schema_database_enabled=*/true,
+            /*enable_schema_database=*/true)));
+
+class IcingSearchEngineInitializationChangeEnableJoinIndexV3FlagTest
+    : public IcingSearchEngineInitializationTest,
+      public ::testing::WithParamInterface<std::vector<bool>> {};
+TEST_P(IcingSearchEngineInitializationChangeEnableJoinIndexV3FlagTest,
+       ChangeEnableJoinIndexV3FlagTest) {
+  std::vector<bool> enable_join_index_v3_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_qualified_id_join_index_v3_and_delete_propagate_from(
+        enable_join_index_v3_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_qualified_id_join_index_v3_and_propagate_delete flags.
+  for (int i = 1; i < enable_join_index_v3_flags.size(); ++i) {
+    bool flag_changed =
+        enable_join_index_v3_flags[i] != enable_join_index_v3_flags[i - 1];
+
+    // Ensure that the qualified id join index is rebuilt if the flag is
+    // changed.
+    IcingSearchEngineOptions options = GetDefaultIcingOptions();
+    options.set_enable_qualified_id_join_index_v3_and_delete_propagate_from(
+        enable_join_index_v3_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));
+
+    // 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(
+    IcingSearchEngineInitializationChangeEnableJoinIndexV3FlagTest,
+    IcingSearchEngineInitializationChangeEnableJoinIndexV3FlagTest,
+    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 4a9dd71..f5c1ced 100644
--- a/icing/icing-search-engine_optimize_test.cc
+++ b/icing/icing-search-engine_optimize_test.cc
@@ -19,6 +19,7 @@
 #include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "gmock/gmock.h"
@@ -52,10 +53,10 @@
 #include "icing/store/document-log-creator.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/jni-test-helpers.h"
 #include "icing/testing/test-data.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/icu-data-file-helper.h"
 
 namespace icing {
 namespace lib {
@@ -63,6 +64,7 @@
 namespace {
 
 using ::icing::lib::portable_equals_proto::EqualsProto;
+using ::testing::ElementsAre;
 using ::testing::Eq;
 using ::testing::Ge;
 using ::testing::Gt;
@@ -100,7 +102,7 @@
       std::string icu_data_file_path =
           GetTestFilePath("icing/icu.dat");
       ICING_ASSERT_OK(
-          icu_data_file_helper::SetUpICUDataFile(icu_data_file_path));
+          icu_data_file_helper::SetUpIcuDataFile(icu_data_file_path));
     }
     filesystem_.CreateDirectoryRecursively(GetTestBaseDir().c_str());
   }
@@ -120,7 +122,10 @@
 
 IcingSearchEngineOptions GetDefaultIcingOptions() {
   IcingSearchEngineOptions icing_options;
+  icing_options.set_enable_scorable_properties(true);
   icing_options.set_base_dir(GetTestBaseDir());
+  icing_options.set_enable_qualified_id_join_index_v3_and_delete_propagate_from(
+      true);
   return icing_options;
 }
 
@@ -130,6 +135,25 @@
   return scoring_spec;
 }
 
+void AddSchemaTypeAliasMap(ScoringSpecProto* scoring_spec,
+                           const std::string& alias_schema_type,
+                           const std::vector<std::string>& schema_types) {
+  SchemaTypeAliasMapProto* alias_map_proto =
+      scoring_spec->add_schema_type_alias_map_protos();
+  alias_map_proto->set_alias_schema_type(alias_schema_type);
+  alias_map_proto->mutable_schema_types()->Add(schema_types.begin(),
+                                               schema_types.end());
+}
+
+std::vector<double> GetScoresFromSearchResults(
+    const SearchResultProto& search_result_proto) {
+  std::vector<double> result_scores;
+  for (int i = 0; i < search_result_proto.results_size(); i++) {
+    result_scores.push_back(search_result_proto.results(i).score());
+  }
+  return result_scores;
+}
+
 // TODO(b/272145329): create SearchSpecBuilder, JoinSpecBuilder,
 // SearchResultProtoBuilder and ResultProtoBuilder for unit tests and build all
 // instances by them.
@@ -1842,6 +1866,268 @@
   }
 }
 
+TEST_F(IcingSearchEngineOptimizeTest,
+       GetScorablePropertyShouldWorkAfterOptimization) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("subject")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("scoreInt64")
+                                        .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                        .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                        .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("foo", "1")
+                                .SetSchema("Email")
+                                .AddStringProperty("subject", "subject foo1")
+                                .AddInt64Property("scoreInt64", 1)
+                                .SetCreationTimestampMs(0)
+                                .Build();
+  double ranking_score_doc1 = 1;
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("foo", "2")
+                                .SetSchema("Email")
+                                .AddStringProperty("subject", "subject foo2")
+                                .AddInt64Property("scoreInt64", 2)
+                                .SetCreationTimestampMs(0)
+                                .Build();
+  double ranking_score_doc2 = 2;
+
+  IcingSearchEngine icing(GetDefaultIcingOptions(), 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());
+
+  {
+    // getScorableProperty works as expected before optimization.
+    SearchSpecProto search_spec;
+    ScoringSpecProto scoring_spec;
+    scoring_spec.set_rank_by(
+        ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+    scoring_spec.set_advanced_scoring_expression(
+        "sum(getScorableProperty(\"Email\", \"scoreInt64\"))");
+    scoring_spec.add_scoring_feature_types_enabled(
+        ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+    AddSchemaTypeAliasMap(&scoring_spec, "Email", {"Email"});
+
+    SearchResultProto search_result_proto = icing.Search(
+        search_spec, scoring_spec, ResultSpecProto::default_instance());
+    EXPECT_THAT(search_result_proto.status(), ProtoIsOk());
+    EXPECT_THAT(GetScoresFromSearchResults(search_result_proto),
+                ElementsAre(ranking_score_doc2, ranking_score_doc1));
+  }
+
+  {
+    // getScorableProperty works as expected after optimization.
+    icing.Optimize();
+    SearchSpecProto search_spec;
+    ScoringSpecProto scoring_spec;
+    scoring_spec.set_rank_by(
+        ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+    scoring_spec.set_advanced_scoring_expression(
+        "sum(getScorableProperty(\"Email\", \"scoreInt64\"))");
+    scoring_spec.add_scoring_feature_types_enabled(
+        ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+    AddSchemaTypeAliasMap(&scoring_spec, "Email", {"Email"});
+
+    SearchResultProto search_result_proto = icing.Search(
+        search_spec, scoring_spec, ResultSpecProto::default_instance());
+    EXPECT_THAT(search_result_proto.status(), ProtoIsOk());
+    EXPECT_THAT(GetScoresFromSearchResults(search_result_proto),
+                ElementsAre(ranking_score_doc2, ranking_score_doc1));
+  }
+}
+
+TEST_F(IcingSearchEngineOptimizeTest,
+       GetScorablePropertyShouldWorkAfterOverwriteAndOptimization) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("subject")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("scoreInt64")
+                                        .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                        .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                        .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("foo", "1")
+                                .SetSchema("Email")
+                                .AddStringProperty("subject", "subject foo1")
+                                .AddInt64Property("scoreInt64", 1)
+                                .SetCreationTimestampMs(0)
+                                .Build();
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("foo", "2")
+                                .SetSchema("Email")
+                                .AddStringProperty("subject", "subject foo2")
+                                .AddInt64Property("scoreInt64", 2)
+                                .SetCreationTimestampMs(0)
+                                .Build();
+  double ranking_score_doc2 = 2;
+  DocumentProto document_overwrite_doc1 =
+      DocumentBuilder()
+          .SetKey("foo", "1")
+          .SetSchema("Email")
+          .AddStringProperty("subject", "subject foo1")
+          .AddInt64Property("scoreInt64", 3)
+          .SetCreationTimestampMs(0)
+          .Build();
+  double ranking_score_doc1_after_overwrite = 3;
+
+  IcingSearchEngine icing(GetDefaultIcingOptions(), 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.Put(document_overwrite_doc1).status(), ProtoIsOk());
+
+  {
+    // getScorableProperty works as expected before optimization.
+    SearchSpecProto search_spec;
+    ScoringSpecProto scoring_spec;
+    scoring_spec.set_rank_by(
+        ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+    scoring_spec.set_advanced_scoring_expression(
+        "sum(getScorableProperty(\"Email\", \"scoreInt64\"))");
+    scoring_spec.add_scoring_feature_types_enabled(
+        ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+    AddSchemaTypeAliasMap(&scoring_spec, "Email", {"Email"});
+
+    SearchResultProto search_result_proto = icing.Search(
+        search_spec, scoring_spec, ResultSpecProto::default_instance());
+    EXPECT_THAT(search_result_proto.status(), ProtoIsOk());
+    EXPECT_THAT(
+        GetScoresFromSearchResults(search_result_proto),
+        ElementsAre(ranking_score_doc1_after_overwrite, ranking_score_doc2));
+  }
+
+  {
+    // getScorableProperty works as expected after optimization.
+    icing.Optimize();
+    SearchSpecProto search_spec;
+    ScoringSpecProto scoring_spec;
+    scoring_spec.set_rank_by(
+        ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+    scoring_spec.set_advanced_scoring_expression(
+        "sum(getScorableProperty(\"Email\", \"scoreInt64\"))");
+    scoring_spec.add_scoring_feature_types_enabled(
+        ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+    AddSchemaTypeAliasMap(&scoring_spec, "Email", {"Email"});
+
+    SearchResultProto search_result_proto = icing.Search(
+        search_spec, scoring_spec, ResultSpecProto::default_instance());
+    EXPECT_THAT(search_result_proto.status(), ProtoIsOk());
+    EXPECT_THAT(
+        GetScoresFromSearchResults(search_result_proto),
+        ElementsAre(ranking_score_doc1_after_overwrite, ranking_score_doc2));
+  }
+}
+
+TEST_F(IcingSearchEngineOptimizeTest,
+       GetScorablePropertyShouldWorkAfterDeletionAndOptimization) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("subject")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("scoreInt64")
+                                        .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                        .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                        .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("foo", "1")
+                                .SetSchema("Email")
+                                .AddStringProperty("subject", "subject foo1")
+                                .AddInt64Property("scoreInt64", 1)
+                                .SetCreationTimestampMs(0)
+                                .Build();
+  double ranking_score_doc1 = 1;
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("foo", "2")
+                                .SetSchema("Email")
+                                .AddStringProperty("subject", "subject foo2")
+                                .AddInt64Property("scoreInt64", 2)
+                                .SetCreationTimestampMs(0)
+                                .Build();
+  double ranking_score_doc2 = 2;
+
+  IcingSearchEngine icing(GetDefaultIcingOptions(), 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());
+
+  {
+    // getScorableProperty works as expected before deletion and optimization.
+    SearchSpecProto search_spec;
+    ScoringSpecProto scoring_spec;
+    scoring_spec.set_rank_by(
+        ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+    scoring_spec.set_advanced_scoring_expression(
+        "sum(getScorableProperty(\"Email\", \"scoreInt64\"))");
+    scoring_spec.add_scoring_feature_types_enabled(
+        ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+    AddSchemaTypeAliasMap(&scoring_spec, "Email", {"Email"});
+
+    SearchResultProto search_result_proto = icing.Search(
+        search_spec, scoring_spec, ResultSpecProto::default_instance());
+    EXPECT_THAT(search_result_proto.status(), ProtoIsOk());
+    EXPECT_THAT(GetScoresFromSearchResults(search_result_proto),
+                ElementsAre(ranking_score_doc2, ranking_score_doc1));
+  }
+
+  {
+    ASSERT_THAT(icing.Delete(document1.namespace_(), document1.uri()).status(),
+                ProtoIsOk());
+    icing.Optimize();
+
+    // After document1 is deleted, the document2 will get a new id internally.
+    // After the Optimize() is called, we verify that the scorable property is
+    // still working as expected.
+    SearchSpecProto search_spec;
+    ScoringSpecProto scoring_spec;
+    scoring_spec.set_rank_by(
+        ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+    scoring_spec.set_advanced_scoring_expression(
+        "sum(getScorableProperty(\"Email\", \"scoreInt64\"))");
+    scoring_spec.add_scoring_feature_types_enabled(
+        ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+    AddSchemaTypeAliasMap(&scoring_spec, "Email", {"Email"});
+
+    SearchResultProto search_result_proto = icing.Search(
+        search_spec, scoring_spec, ResultSpecProto::default_instance());
+    EXPECT_THAT(search_result_proto.status(), ProtoIsOk());
+    EXPECT_THAT(GetScoresFromSearchResults(search_result_proto),
+                ElementsAre(ranking_score_doc2));
+  }
+}
+
 }  // namespace
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/icing-search-engine_put_test.cc b/icing/icing-search-engine_put_test.cc
index c5f8e2b..a6cc463 100644
--- a/icing/icing-search-engine_put_test.cc
+++ b/icing/icing-search-engine_put_test.cc
@@ -47,11 +47,11 @@
 #include "icing/schema-builder.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/jni-test-helpers.h"
 #include "icing/testing/random-string.h"
 #include "icing/testing/test-data.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/icu-data-file-helper.h"
 
 namespace icing {
 namespace lib {
@@ -111,7 +111,7 @@
       std::string icu_data_file_path =
           GetTestFilePath("icing/icu.dat");
       ICING_ASSERT_OK(
-          icu_data_file_helper::SetUpICUDataFile(icu_data_file_path));
+          icu_data_file_helper::SetUpIcuDataFile(icu_data_file_path));
     }
     filesystem_.CreateDirectoryRecursively(GetTestBaseDir().c_str());
   }
@@ -528,7 +528,7 @@
 
   PropertyProto::BlobHandleProto blob_handle;
   blob_handle.set_digest(std::string(32, ' '));
-  blob_handle.set_label("label");
+  blob_handle.set_namespace_("namespace");
 
   DocumentProto document =
       DocumentBuilder()
@@ -570,7 +570,7 @@
 
   PropertyProto::BlobHandleProto blob_handle;
   blob_handle.set_digest("invalid digest");
-  blob_handle.set_label("label");
+  blob_handle.set_namespace_("namespace");
 
   DocumentProto document =
       DocumentBuilder()
diff --git a/icing/icing-search-engine_schema_test.cc b/icing/icing-search-engine_schema_test.cc
index 115e4cf..89f9546 100644
--- a/icing/icing-search-engine_schema_test.cc
+++ b/icing/icing-search-engine_schema_test.cc
@@ -17,6 +17,7 @@
 #include <memory>
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
@@ -26,6 +27,7 @@
 #include "icing/icing-search-engine.h"
 #include "icing/jni/jni-cache.h"
 #include "icing/join/join-processor.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"
@@ -46,10 +48,11 @@
 #include "icing/schema/section.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.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 {
@@ -92,7 +95,7 @@
       std::string icu_data_file_path =
           GetTestFilePath("icing/icu.dat");
       ICING_ASSERT_OK(
-          icu_data_file_helper::SetUpICUDataFile(icu_data_file_path));
+          icu_data_file_helper::SetUpIcuDataFile(icu_data_file_path));
     }
     filesystem_.CreateDirectoryRecursively(GetTestBaseDir().c_str());
   }
@@ -116,7 +119,9 @@
   IcingSearchEngineOptions icing_options;
   icing_options.set_base_dir(GetTestBaseDir());
   icing_options.set_document_store_namespace_id_fingerprint(true);
-  icing_options.set_use_new_qualified_id_join_index(true);
+  icing_options.set_enable_schema_database(true);
+  icing_options.set_enable_qualified_id_join_index_v3_and_delete_propagate_from(
+      true);
   return icing_options;
 }
 
@@ -829,6 +834,368 @@
       EqualsSearchResultIgnoreStatsAndScores(expected_search_result_proto3));
 }
 
+TEST_F(IcingSearchEngineSchemaTest, SetSchemaMultipleDatabases) {
+  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(db1_schema);
+  // Ignore latency numbers. They're covered elsewhere.
+  set_schema_result.clear_latency_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");
+  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));
+
+  // Add a schema for db2:
+  // - 'a': string type, indexed.
+  // - 'c': int64 type, indexed.
+  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 db2_schema = SchemaBuilder().AddType(db2_type).Build();
+  set_schema_result = icing.SetSchema(db2_schema);
+  // Ignore latency numbers. They're covered elsewhere.
+  set_schema_result.clear_latency_ms();
+  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");
+  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() =
+      SchemaBuilder().AddType(db1_type).AddType(db2_type).Build();
+  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() = db1_schema;
+  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() = db2_schema;
+  EXPECT_THAT(icing.GetSchema("db2"),
+              EqualsProto(expected_get_schema_result_proto_db2_full));
+}
+
+TEST_F(IcingSearchEngineSchemaTest, SetSchemaUpdateExistingDatabaseOk) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  // Create and set schema in db1 with 2 properties:
+  // - 'b': string type, indexed.
+  // - 'c': int64 type, indexed.
+  SchemaTypeConfigProto db1_type =
+      SchemaTypeConfigBuilder()
+          .SetType("db1_type")
+          .SetDatabase("db1")
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("b")
+                           .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                           .SetCardinality(CARDINALITY_REQUIRED))
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("c")
+                           .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                           .SetCardinality(CARDINALITY_REQUIRED))
+          .Build();
+  SchemaProto db1_schema = SchemaBuilder().AddType(db1_type).Build();
+  SetSchemaResultProto set_schema_result = icing.SetSchema(db1_schema);
+  // Ignore latency numbers. They're covered elsewhere.
+  set_schema_result.clear_latency_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");
+  EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
+
+  // Add a schema for db2:
+  // - 'b': string type, indexed.
+  // - 'd': int64 type, indexed.
+  SchemaTypeConfigProto db2_type =
+      SchemaTypeConfigBuilder()
+          .SetType("db2_type")
+          .SetDatabase("db2")
+          .AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("b")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_REQUIRED))
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("d")
+                           .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                           .SetCardinality(CARDINALITY_REQUIRED))
+          .Build();
+  SchemaProto db2_schema = SchemaBuilder().AddType(db2_type).Build();
+  set_schema_result = icing.SetSchema(db2_schema);
+  // Ignore latency numbers. They're covered elsewhere.
+  set_schema_result.clear_latency_ms();
+  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");
+  EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
+
+  // Add documents
+  DocumentProto db1_document1 =
+      DocumentBuilder()
+          .SetKey("namespace", "uri1")
+          .SetSchema("db1_type")
+          .AddStringProperty("b", "message body")
+          .AddInt64Property("c", 123)
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto db2_document =
+      DocumentBuilder()
+          .SetKey("namespace", "uri2")
+          .SetSchema("db2_type")
+          .AddStringProperty("b", "message body")
+          .AddInt64Property("d", 123)
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  EXPECT_THAT(icing.Put(db1_document1).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(db2_document).status(), ProtoIsOk());
+
+  // Verify term search. Should match both docs.
+  SearchSpecProto search_spec1;
+  search_spec1.set_query("b:message");
+  search_spec1.set_term_match_type(TermMatchType::EXACT_ONLY);
+
+  SearchResultProto expected_result_db1doc1;
+  expected_result_db1doc1.mutable_status()->set_code(StatusProto::OK);
+  *expected_result_db1doc1.mutable_results()->Add()->mutable_document() =
+      db1_document1;
+
+  SearchResultProto expected_result_db1doc1_db2;
+  expected_result_db1doc1_db2.mutable_status()->set_code(StatusProto::OK);
+  *expected_result_db1doc1_db2.mutable_results()->Add()->mutable_document() =
+      db2_document;
+  *expected_result_db1doc1_db2.mutable_results()->Add()->mutable_document() =
+      db1_document1;
+
+  SearchResultProto actual_results =
+      icing.Search(search_spec1, GetDefaultScoringSpec(),
+                   ResultSpecProto::default_instance());
+  EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
+                                  expected_result_db1doc1_db2));
+
+  // Verify numeric (integer) search. Should only match db1_document1.
+  SearchSpecProto search_spec2;
+  search_spec2.set_query("c == 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_result_db1doc1));
+
+  // Update the db1 schema:
+  // - db1_schema:
+  //   - 'a': string type, indexed. (new)
+  //   - 'b': string type, indexed.
+  //   - 'c': int64 type, indexed.
+  // - db2_schema:
+  //   - 'b': string type, indexed.
+  //   - 'd': int64 type, indexed.
+  db1_type.mutable_properties()->Add(
+      PropertyConfigBuilder()
+          .SetName("a")
+          .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+          .SetCardinality(CARDINALITY_OPTIONAL)
+          .Build());
+  db1_schema = SchemaBuilder().AddType(db1_type).Build();
+  set_schema_result = icing.SetSchema(db1_schema);
+  // Ignore latency numbers. They're covered elsewhere.
+  set_schema_result.clear_latency_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("db1_type");
+  EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
+
+  // Add new document
+  DocumentProto db1_document2 =
+      DocumentBuilder()
+          .SetKey("namespace", "uri3")
+          .SetSchema("db1_type")
+          .AddStringProperty("a", "message body")
+          .AddStringProperty("b", "string value")
+          .AddInt64Property("c", 123)
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  EXPECT_THAT(icing.Put(db1_document2).status(), ProtoIsOk());
+
+  // Check that the original docs are still searchable.
+  // Verify term search. "b:message" should still only match db1_document1 and
+  // db2_document.
+  actual_results = icing.Search(search_spec1, GetDefaultScoringSpec(),
+                                ResultSpecProto::default_instance());
+  EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
+                                  expected_result_db1doc1_db2));
+
+  // Verify numeric (integer) search. "c == 123" should now match match
+  // db1_document1 and db1_document2.
+  SearchResultProto expected_result_all_db1;
+  expected_result_all_db1.mutable_status()->set_code(StatusProto::OK);
+  *expected_result_all_db1.mutable_results()->Add()->mutable_document() =
+      db1_document2;
+  *expected_result_all_db1.mutable_results()->Add()->mutable_document() =
+      db1_document1;
+
+  actual_results = icing.Search(search_spec2, GetDefaultScoringSpec(),
+                                ResultSpecProto::default_instance());
+  EXPECT_THAT(actual_results,
+              EqualsSearchResultIgnoreStatsAndScores(expected_result_all_db1));
+
+  // "message" should match all docs
+  SearchSpecProto search_spec3;
+  search_spec3.set_query("message");
+  search_spec3.set_term_match_type(TermMatchType::PREFIX);
+
+  SearchResultProto expected_result_all_docs;
+  expected_result_all_docs.mutable_status()->set_code(StatusProto::OK);
+  *expected_result_all_docs.mutable_results()->Add()->mutable_document() =
+      db1_document2;
+  *expected_result_all_docs.mutable_results()->Add()->mutable_document() =
+      db2_document;
+  *expected_result_all_docs.mutable_results()->Add()->mutable_document() =
+      db1_document1;
+
+  actual_results = icing.Search(search_spec3, GetDefaultScoringSpec(),
+                                ResultSpecProto::default_instance());
+  EXPECT_THAT(actual_results,
+              EqualsSearchResultIgnoreStatsAndScores(expected_result_all_docs));
+
+  // 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() =
+      SchemaBuilder().AddType(db1_type).AddType(db2_type).Build();
+  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() = db1_schema;
+  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() = db2_schema;
+  EXPECT_THAT(icing.GetSchema("db2"),
+              EqualsProto(expected_get_schema_result_proto_db2_full));
+}
+
 TEST_F(IcingSearchEngineSchemaTest,
        SetSchemaNewIndexedStringPropertyTriggersIndexRestorationAndReturnsOk) {
   IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
@@ -2928,6 +3295,125 @@
   EXPECT_THAT(icing.GetSchema(), EqualsProto(expected_get_schema_result_proto));
 }
 
+TEST_F(IcingSearchEngineSchemaTest, GetSchemaDatabaseOk) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  // Add a schema for db1:
+  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(db1_schema);
+  // Ignore latency numbers. They're covered elsewhere.
+  set_schema_result.clear_latency_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");
+  EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
+
+  // Add a schema for db2:
+  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 db2_schema = SchemaBuilder().AddType(db2_type).Build();
+  set_schema_result = icing.SetSchema(db2_schema);
+  // Ignore latency numbers. They're covered elsewhere.
+  set_schema_result.clear_latency_ms();
+  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");
+  EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
+
+  // GetSchema per database
+  GetSchemaResultProto expected_get_schema_result_proto_db1;
+  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"),
+              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"),
+              EqualsProto(expected_get_schema_result_proto_db2));
+
+  // 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() =
+      SchemaBuilder().AddType(db1_type).AddType(db2_type).Build();
+  EXPECT_THAT(icing.GetSchema(),
+              EqualsProto(expected_get_schema_result_proto_full));
+}
+
+TEST_F(IcingSearchEngineSchemaTest, GetSchemaDatabaseNotFound) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+
+  GetSchemaResultProto get_schema_result_proto =
+      icing.GetSchema("nonexistent_db");
+  EXPECT_THAT(get_schema_result_proto.status(),
+              ProtoStatusIs(StatusProto::NOT_FOUND));
+  EXPECT_THAT(get_schema_result_proto.status().message(),
+              HasSubstr("No schema found"));
+
+  // Add a schema for db1:
+  SchemaProto db1_schema =
+      SchemaBuilder()
+          .AddType(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();
+  SetSchemaResultProto set_schema_result = icing.SetSchema(db1_schema);
+  // Ignore latency numbers. They're covered elsewhere.
+  set_schema_result.clear_latency_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");
+  EXPECT_THAT(set_schema_result, EqualsProto(expected_set_schema_result));
+
+  get_schema_result_proto = icing.GetSchema("nonexistent_db");
+  EXPECT_THAT(get_schema_result_proto.status(),
+              ProtoStatusIs(StatusProto::NOT_FOUND));
+  EXPECT_THAT(get_schema_result_proto.status().message(),
+              HasSubstr("No schema found"));
+}
+
 TEST_F(IcingSearchEngineSchemaTest, GetSchemaTypeFailedPrecondition) {
   IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
diff --git a/icing/icing-search-engine_search_test.cc b/icing/icing-search-engine_search_test.cc
index 72121ac..346bb19 100644
--- a/icing/icing-search-engine_search_test.cc
+++ b/icing/icing-search-engine_search_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 <initializer_list>
 #include <limits>
@@ -53,11 +54,11 @@
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/embedding-test-utils.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.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"
 #include "icing/util/snippet-helpers.h"
 
 namespace icing {
@@ -107,7 +108,7 @@
       std::string icu_data_file_path =
           GetTestFilePath("icing/icu.dat");
       ICING_ASSERT_OK(
-          icu_data_file_helper::SetUpICUDataFile(icu_data_file_path));
+          icu_data_file_helper::SetUpIcuDataFile(icu_data_file_path));
     }
     filesystem_.CreateDirectoryRecursively(GetTestBaseDir().c_str());
   }
@@ -131,7 +132,12 @@
   IcingSearchEngineOptions icing_options;
   icing_options.set_base_dir(GetTestBaseDir());
   icing_options.set_document_store_namespace_id_fingerprint(true);
-  icing_options.set_use_new_qualified_id_join_index(true);
+  icing_options.set_enable_schema_database(true);
+  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_qualified_id_join_index_v3_and_delete_propagate_from(
+      true);
   return icing_options;
 }
 
@@ -247,6 +253,25 @@
   return result_uris;
 }
 
+std::vector<int32_t> GetScoresFromSearchResults(
+    SearchResultProto& search_result_proto) {
+  std::vector<int32_t> result_scores;
+  for (int i = 0; i < search_result_proto.results_size(); i++) {
+    result_scores.push_back(search_result_proto.results(i).score());
+  }
+  return result_scores;
+}
+
+void AddSchemaTypeAliasMap(ScoringSpecProto* scoring_spec,
+                           const std::string& alias_schema_type,
+                           const std::vector<std::string>& schema_types) {
+  SchemaTypeAliasMapProto* alias_map_proto =
+      scoring_spec->add_schema_type_alias_map_protos();
+  alias_map_proto->set_alias_schema_type(alias_schema_type);
+  alias_map_proto->mutable_schema_types()->Add(schema_types.begin(),
+                                               schema_types.end());
+}
+
 TEST_F(IcingSearchEngineSearchTest, SearchReturnsValidResults) {
   IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
   ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
@@ -6014,6 +6039,245 @@
               ElementsAre(EqualsProto(expected_result_google::protobuf)));
 }
 
+TEST_F(IcingSearchEngineSearchTest, JoinAfterUpdatingParent) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Person")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("firstName")
+                                        .SetDataTypeString(TERM_MATCH_PREFIX,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("lastName")
+                                        .SetDataTypeString(TERM_MATCH_PREFIX,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("emailAddress")
+                                        .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")
+          .AddStringProperty("firstName", "first")
+          .AddStringProperty("lastName", "last")
+          .AddStringProperty("emailAddress", "email@gmail.com")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetScore(1)
+          .Build();
+  DocumentProto email =
+      DocumentBuilder()
+          .SetKey("namespace", "email")
+          .SetSchema("Email")
+          .AddStringProperty("subject", "test subject")
+          .AddStringProperty("personQualifiedId", "pkg$db/namespace#person")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetScore(3)
+          .Build();
+
+  IcingSearchEngine icing(GetDefaultIcingOptions(), 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(email).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");
+  join_spec->set_aggregation_scoring_strategy(
+      JoinSpecProto::AggregationScoringStrategy::MAX);
+  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_num_per_page(1);
+  result_spec.set_max_joined_children_per_parent_to_return(
+      std::numeric_limits<int32_t>::max());
+
+  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));
+
+  // Put person document again to update the parent document.
+  DocumentProto updated_person =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "person")
+          .SetSchema("Person")
+          .AddStringProperty("firstName", "first updated")
+          .AddStringProperty("lastName", "last")
+          .AddStringProperty("emailAddress", "email@gmail.com")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetScore(1)
+          .Build();
+  ASSERT_THAT(icing.Put(updated_person).status(), ProtoIsOk());
+
+  // Search again to verify join API still works.
+  *expected_result.mutable_results(0)->mutable_document() = updated_person;
+  EXPECT_THAT(icing.Search(search_spec, scoring_spec, result_spec),
+              EqualsSearchResultIgnoreStatsAndScores(expected_result));
+}
+
+TEST_F(IcingSearchEngineSearchTest, JoinAfterUpdatingChild) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Person")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("firstName")
+                                        .SetDataTypeString(TERM_MATCH_PREFIX,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("lastName")
+                                        .SetDataTypeString(TERM_MATCH_PREFIX,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("emailAddress")
+                                        .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")
+          .AddStringProperty("firstName", "first")
+          .AddStringProperty("lastName", "last")
+          .AddStringProperty("emailAddress", "email@gmail.com")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetScore(1)
+          .Build();
+  DocumentProto email =
+      DocumentBuilder()
+          .SetKey("namespace", "email")
+          .SetSchema("Email")
+          .AddStringProperty("subject", "test subject")
+          .AddStringProperty("personQualifiedId", "pkg$db/namespace#person")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetScore(3)
+          .Build();
+
+  IcingSearchEngine icing(GetDefaultIcingOptions(), 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(email).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");
+  join_spec->set_aggregation_scoring_strategy(
+      JoinSpecProto::AggregationScoringStrategy::MAX);
+  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_num_per_page(1);
+  result_spec.set_max_joined_children_per_parent_to_return(
+      std::numeric_limits<int32_t>::max());
+
+  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));
+
+  // Put person document again to update the parent document.
+  DocumentProto updated_email =
+      DocumentBuilder()
+          .SetKey("namespace", "email")
+          .SetSchema("Email")
+          .AddStringProperty("subject", "test subject updated")
+          .AddStringProperty("personQualifiedId", "pkg$db/namespace#person")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetScore(3)
+          .Build();
+  ASSERT_THAT(icing.Put(updated_email).status(), ProtoIsOk());
+
+  // Search again to verify join API still works.
+  *expected_result.mutable_results(0)
+       ->mutable_joined_results(0)
+       ->mutable_document() = updated_email;
+  EXPECT_THAT(icing.Search(search_spec, scoring_spec, result_spec),
+              EqualsSearchResultIgnoreStatsAndScores(expected_result));
+}
+
 TEST_F(IcingSearchEngineSearchTest, JoinSnippet) {
   SchemaProto schema =
       SchemaBuilder()
@@ -7621,6 +7885,124 @@
               DoubleNear(0 + 0.5, kEps));
 }
 
+TEST_F(IcingSearchEngineSearchTest, EmbeddingSearchWithQuantizedProperty) {
+  constexpr float eps = 0.0001f;
+
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Email")
+                       .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();
+  // If quantization is enabled, this vector will be quantized to {0, 1, 255}.
+  PropertyProto::VectorProto vector1 = CreateVector("my_model", {0, 1.45, 255});
+  // If quantization is enabled, this vector will be quantized to {0, -2, -255}.
+  PropertyProto::VectorProto vector2 =
+      CreateVector("my_model", {0, -2.15, -255});
+
+  DocumentProto document_with_original_embedding =
+      DocumentBuilder()
+          .SetKey("icing", "uri0")
+          .SetSchema("Email")
+          .SetCreationTimestampMs(1)
+          .AddStringProperty("body", "foo")
+          .AddVectorProperty("embedding", vector1, vector2)
+          .Build();
+  DocumentProto document_with_quantized_embedding =
+      DocumentBuilder()
+          .SetKey("icing", "uri1")
+          .SetSchema("Email")
+          .SetCreationTimestampMs(1)
+          .AddVectorProperty("embeddingQuantized", vector1, vector2)
+          .Build();
+
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document_with_original_embedding).status(),
+              ProtoIsOk());
+  ASSERT_THAT(icing.Put(document_with_quantized_embedding).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));
+  // Add an embedding query with semantic scores:
+  // - [0 + 1.45 + 255 = 256.45, 0 - 2.15 - 255 = -257.15] for
+  //   document_with_original_embedding
+  // - [0 + 1 + 255 = 256, 0 - 2 - 255 = -257] for
+  //   document_with_quantized_embedding
+  *search_spec.add_embedding_query_vectors() =
+      CreateVector("my_model", {1, 1, 1});
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+  scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+
+  // All embeddings should be matched for the range of [-1000, 1000].
+  search_spec.set_query(
+      "semanticSearch(getEmbeddingParameter(0), -1000, 1000)");
+  scoring_spec.set_advanced_scoring_expression(
+      "len(this.matchedSemanticScores(getEmbeddingParameter(0)))");
+  scoring_spec.add_additional_advanced_scoring_expressions(
+      "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))");
+  SearchResultProto results = icing.Search(search_spec, scoring_spec,
+                                           ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+  // Check results for document_with_quantized_embedding.
+  EXPECT_THAT(results.results(0).document(),
+              EqualsProto(document_with_quantized_embedding));
+  EXPECT_THAT(results.results(0).score(), 2);
+  EXPECT_THAT(results.results(0).additional_scores(0),
+              DoubleNear(256 + (-257), eps));
+  // Check results for document_with_original_embedding.
+  EXPECT_THAT(results.results(1).document(),
+              EqualsProto(document_with_original_embedding));
+  EXPECT_THAT(results.results(1).score(), 2);
+  EXPECT_THAT(results.results(1).additional_scores(0),
+              DoubleNear(256.45 + (-257.15), eps));
+
+  // Only one embedding (with score of 256.45, or 256 if quantized) should be
+  // matched for the range of [0, 1000].
+  search_spec.set_query("semanticSearch(getEmbeddingParameter(0), 0, 1000)");
+  scoring_spec.set_advanced_scoring_expression(
+      "len(this.matchedSemanticScores(getEmbeddingParameter(0)))");
+  scoring_spec.add_additional_advanced_scoring_expressions(
+      "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))");
+  results = icing.Search(search_spec, scoring_spec,
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(2));
+  // Check results for document_with_quantized_embedding.
+  EXPECT_THAT(results.results(0).document(),
+              EqualsProto(document_with_quantized_embedding));
+  EXPECT_THAT(results.results(0).score(), 1);
+  EXPECT_THAT(results.results(0).additional_scores(0), DoubleNear(256, eps));
+  // Check results for document_with_original_embedding.
+  EXPECT_THAT(results.results(1).document(),
+              EqualsProto(document_with_original_embedding));
+  EXPECT_THAT(results.results(1).score(), 1);
+  EXPECT_THAT(results.results(1).additional_scores(0), DoubleNear(256.45, eps));
+}
+
 TEST_F(IcingSearchEngineSearchTest,
        AdditionalScoresOnlyAllowedInAdvancedScoring) {
   SchemaProto schema =
@@ -7823,6 +8205,1930 @@
   EXPECT_THAT(results.results(), IsEmpty());
 }
 
+TEST_F(IcingSearchEngineSearchTest, SearchWithUriFilters) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  // Add three documents with different URIs, namespaces, and "body" values.
+  DocumentProto document_one =
+      DocumentBuilder()
+          .SetKey("namespace1", "uri1")
+          .SetSchema("Message")
+          .AddStringProperty("body", "foo")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk());
+  DocumentProto document_two =
+      DocumentBuilder()
+          .SetKey("namespace2", "uri2")
+          .SetSchema("Message")
+          .AddStringProperty("body", "foo")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk());
+  DocumentProto document_three =
+      DocumentBuilder()
+          .SetKey("namespace2", "uri3")
+          .SetSchema("Message")
+          .AddStringProperty("body", "bar")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  ASSERT_THAT(icing.Put(document_three).status(), ProtoIsOk());
+
+  // Create a search spec with an empty query and uri filters that should match
+  // document1 and document3.
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::PREFIX);
+  search_spec.set_query("");
+  NamespaceDocumentUriGroup* namespace1_uris =
+      search_spec.add_document_uri_filters();
+  namespace1_uris->set_namespace_("namespace1");
+  namespace1_uris->add_document_uris("uri1");
+  NamespaceDocumentUriGroup* namespace2_uris =
+      search_spec.add_document_uri_filters();
+  namespace2_uris->set_namespace_("namespace2");
+  namespace2_uris->add_document_uris("uri3");
+
+  // Check results
+  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(document_three));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(document_one));
+
+  // Now set the query to "foo" and check that only document_one is returned.
+  search_spec.set_query("foo");
+  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,
+       NamespacesInUriFiltersMustAppearInNamespaceFilters) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  // Add three documents with different URIs and namespaces.
+  DocumentProto document_one =
+      DocumentBuilder()
+          .SetKey("namespace1", "uri1")
+          .SetSchema("Message")
+          .AddStringProperty("body", "foo")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk());
+  DocumentProto document_two =
+      DocumentBuilder()
+          .SetKey("namespace2", "uri2")
+          .SetSchema("Message")
+          .AddStringProperty("body", "foo")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk());
+  DocumentProto document_three =
+      DocumentBuilder()
+          .SetKey("namespace2", "uri3")
+          .SetSchema("Message")
+          .AddStringProperty("body", "foo")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  ASSERT_THAT(icing.Put(document_three).status(), ProtoIsOk());
+
+  // Create a search spec with uri filters that include a namespace that is not
+  // in the namespace filters.
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::PREFIX);
+  search_spec.set_query("foo");
+  NamespaceDocumentUriGroup* namespace1_uris =
+      search_spec.add_document_uri_filters();
+  namespace1_uris->set_namespace_("namespace1");
+  namespace1_uris->add_document_uris("uri1");
+  NamespaceDocumentUriGroup* namespace2_uris =
+      search_spec.add_document_uri_filters();
+  namespace2_uris->set_namespace_("namespace2");
+  namespace2_uris->add_document_uris("uri3");
+  search_spec.add_namespace_filters("namespace1");
+
+  // Check results
+  SearchResultProto results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                                           ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status().message(),
+              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()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Person")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("name")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  DocumentProto document0 = DocumentBuilder()
+                                .SetKey("icing", "person0")
+                                .SetSchema("Person")
+                                .SetScore(10)
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("name", "John")
+                                .AddDoubleProperty("income", 10000, 20000)
+                                .Build();
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("icing", "person1")
+                                .SetSchema("Person")
+                                .SetScore(10)
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("name", "Johnson")
+                                .AddDoubleProperty("income", 5000, 30000)
+                                .Build();
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("icing", "person2")
+                                .SetSchema("Person")
+                                .SetScore(20)
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("name", "Jack")
+                                .Build();
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document0).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document1).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document2).status(), ProtoIsOk());
+
+  // "J" will match all 3 documents
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::PREFIX);
+  search_spec.set_query("J");
+
+  // Ranking by documentScore and income.
+  ScoringSpecProto scoring_spec_by_income = GetDefaultScoringSpec();
+  scoring_spec_by_income.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  scoring_spec_by_income.set_advanced_scoring_expression(
+      "this.documentScore() + sum(getScorableProperty(\"Person\", "
+      "\"income\"))");
+  scoring_spec_by_income.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  AddSchemaTypeAliasMap(&scoring_spec_by_income, "Person", {"Person"});
+
+  int expected_person0_score = 10 + 10000 + 20000;
+  int expected_person1_score = 10 + 5000 + 30000;
+  int expected_person2_score = 20 + 0;
+
+  SearchResultProto search_result_proto = icing.Search(
+      search_spec, scoring_spec_by_income, ResultSpecProto::default_instance());
+  EXPECT_THAT(search_result_proto.status(), ProtoIsOk());
+
+  // Verify that the search results are ranked as expected.
+  EXPECT_THAT(GetUrisFromSearchResults(search_result_proto),
+              ElementsAre("person1", "person0", "person2"));
+  EXPECT_THAT(GetScoresFromSearchResults(search_result_proto),
+              ElementsAre(expected_person1_score, expected_person0_score,
+                          expected_person2_score));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       SearchWithRankingByScorablePropertyWithFeatureDisabled) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Person")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("name")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+
+  // Ranking by documentScore and income.
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+  scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  scoring_spec.set_advanced_scoring_expression(
+      "this.documentScore() + sum(getScorableProperty(\"Person\", "
+      "\"income\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "Person", {"Person"});
+
+  SearchResultProto expected_search_result_proto;
+  expected_search_result_proto.mutable_status()->set_code(
+      StatusProto::INVALID_ARGUMENT);
+  expected_search_result_proto.mutable_status()->set_message(
+      "SCORABLE_PROPERTY_RANKING feature is not enabled.");
+
+  SearchResultProto actual_results = icing.Search(
+      search_spec, scoring_spec, ResultSpecProto::default_instance());
+  EXPECT_THAT(actual_results, EqualsSearchResultIgnoreStatsAndScores(
+                                  expected_search_result_proto));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       SearchWithRankingByScorableProperty_WithNestedSchemas) {
+  SchemaProto schema_proto =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Person")
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("id")
+                                   .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                   .SetCardinality(CARDINALITY_REPEATED))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("age")
+                          .SetDataType(PropertyConfigProto::DataType::INT64)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("isStarred")
+                          .SetDataType(PropertyConfigProto::DataType::BOOLEAN)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("subject")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(
+                           PropertyConfigBuilder()
+                               .SetName("sender")
+                               .SetDataTypeDocument(
+                                   "Person", /*index_nested_properties=*/true)
+                               .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(
+                           PropertyConfigBuilder()
+                               .SetName("receiver")
+                               .SetDataTypeDocument(
+                                   "Person", /*index_nested_properties=*/true)
+                               .SetCardinality(CARDINALITY_REPEATED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("scoreInt64")
+                                        .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                        .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                        .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  DocumentProto document =
+      DocumentBuilder()
+          .SetKey("namespace", "email_id0")
+          .SetSchema("Email")
+          .SetScore(5)
+          .AddInt64Property("scoreInt64", 1, 2, 3)
+          .AddDocumentProperty("receiver",
+                               DocumentBuilder()
+                                   .SetKey("namespace", "receiver0")
+                                   .SetSchema("Person")
+                                   .AddInt64Property("age", 30)
+                                   .AddDoubleProperty("income", 10, 20)
+                                   .AddBooleanProperty("isStarred", true)
+                                   .Build(),
+                               DocumentBuilder()
+                                   .SetKey("namespace", "receiver1")
+                                   .SetSchema("Person")
+                                   .AddInt64Property("age", 35)
+                                   .AddDoubleProperty("income", 11, 12)
+                                   .AddBooleanProperty("isStarred", false)
+                                   .Build())
+          .AddDocumentProperty("sender",
+                               DocumentBuilder()
+                                   .SetKey("namespace", "sender0")
+                                   .SetSchema("Person")
+                                   .AddInt64Property("age", 50)
+                                   .AddDoubleProperty("income", 21, 22)
+                                   .AddBooleanProperty("isStarred", false)
+                                   .Build())
+          .Build();
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(schema_proto).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+  scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  scoring_spec.set_advanced_scoring_expression(
+      "this.documentScore() + "
+      "sum(getScorableProperty(\"Email\", \"scoreInt64\")) + "
+      "sum(getScorableProperty(\"Email\", \"sender.income\")) + "
+      "5 * sum(getScorableProperty(\"Email\", \"receiver.income\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "Person", {"Person"});
+  AddSchemaTypeAliasMap(&scoring_spec, "Email", {"Email"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  int expected_score = 5 + (1 + 2 + 3) + (21 + 22) + 5 * (10 + 20 + 11 + 12);
+
+  SearchResultProto search_result_proto = icing.Search(
+      search_spec, scoring_spec, ResultSpecProto::default_instance());
+  EXPECT_THAT(search_result_proto.status(), ProtoIsOk());
+
+  EXPECT_THAT(GetUrisFromSearchResults(search_result_proto),
+              ElementsAre("email_id0"));
+  EXPECT_THAT(GetScoresFromSearchResults(search_result_proto),
+              ElementsAre(expected_score));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       SearchWithRankingByScorableProperty_WithInvalidPropertyName) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Person")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("name")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  DocumentProto document0 = DocumentBuilder()
+                                .SetKey("icing", "person0")
+                                .SetSchema("Person")
+                                .SetScore(10)
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("name", "John")
+                                .AddDoubleProperty("income", 10000, 20000)
+                                .Build();
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document0).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+  scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  scoring_spec.set_advanced_scoring_expression(
+      "this.documentScore() + sum(getScorableProperty(\"Person\", "
+      "\"not_exist\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "Person", {"Person"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  SearchResultProto expected_search_result_proto;
+  expected_search_result_proto.mutable_status()->set_code(
+      StatusProto::INVALID_ARGUMENT);
+  expected_search_result_proto.mutable_status()->set_message(
+      "'not_exist' is not defined as a scorable property under schema type 0");
+
+  SearchResultProto actual_search_result_proto = icing.Search(
+      search_spec, scoring_spec, ResultSpecProto::default_instance());
+  EXPECT_THAT(
+      actual_search_result_proto,
+      EqualsSearchResultIgnoreStatsAndScores(expected_search_result_proto));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       SearchWithRankingByScorableProperty_ScorablePropertyDisabled) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Person")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("name")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  DocumentProto document0 = DocumentBuilder()
+                                .SetKey("icing", "person0")
+                                .SetSchema("Person")
+                                .SetScore(10)
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("name", "John")
+                                .AddDoubleProperty("income", 10000, 20000)
+                                .Build();
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_scorable_properties(false);
+  IcingSearchEngine icing(options, GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document0).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+  scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  scoring_spec.set_advanced_scoring_expression(
+      "this.documentScore() + sum(getScorableProperty(\"Person\", "
+      "\"not_exist\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "Person", {"Person"});
+
+  SearchResultProto results = icing.Search(search_spec, scoring_spec,
+                                           ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
+}
+
+TEST_F(IcingSearchEngineSearchTest, JoinSearchWithRankingByScorableProperty) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Person")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("name")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("customizedScore")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("isStarred")
+                          .SetDataType(PropertyConfigProto::DataType::BOOLEAN)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("CallLogSignalDoc")
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("personQualifiedId")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                   .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("rfsScore")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("SmsLogSignalDoc")
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("personQualifiedId")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                   .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("rfsScore")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+
+  // John has 2 call logs and 1 sms log to join.
+  DocumentProto person_john_doc =
+      DocumentBuilder()
+          .SetKey("namespace", "person_john_id")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetSchema("Person")
+          .SetScore(10)
+          .AddBooleanProperty("isStarred", true)
+          .AddDoubleProperty("customizedScore", 30)
+          .AddStringProperty("name", "John")
+          .Build();
+  // Kevin has 2 call logs and 1 sms log to join.
+  DocumentProto person_kevin_doc =
+      DocumentBuilder()
+          .SetKey("namespace", "person_kevin_id")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetSchema("Person")
+          .SetScore(20)
+          .AddBooleanProperty("isStarred", false)
+          .AddDoubleProperty("customizedScore", 40)
+          .AddStringProperty("name", "Kevin")
+          .Build();
+  // Tim has no signal docs to join.
+  DocumentProto person_tim_doc =
+      DocumentBuilder()
+          .SetKey("namespace", "person_tim_id")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetSchema("Person")
+          .SetScore(50)
+          .AddDoubleProperty("customizedScore", 60)
+          .AddStringProperty("name", "Tim")
+          .Build();
+  DocumentProto john_call_log_doc1 =
+      DocumentBuilder()
+          .SetKey("namespace", "call_log_id1")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetSchema("CallLogSignalDoc")
+          .SetScore(5)
+          .AddDoubleProperty("rfsScore", 100, 200)
+          .AddStringProperty("personQualifiedId", "namespace#person_john_id")
+          .Build();
+  DocumentProto john_call_log_doc2 =
+      DocumentBuilder()
+          .SetKey("namespace", "call_log_id2")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetSchema("CallLogSignalDoc")
+          .SetScore(5)
+          .AddDoubleProperty("rfsScore", 200, 300)
+          .AddStringProperty("personQualifiedId", "namespace#person_john_id")
+          .Build();
+  DocumentProto kevin_call_log_doc1 =
+      DocumentBuilder()
+          .SetKey("namespace", "call_log_id3")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetSchema("CallLogSignalDoc")
+          .SetScore(5)
+          .AddDoubleProperty("rfsScore", 300, 400)
+          .AddStringProperty("personQualifiedId", "namespace#person_kevin_id")
+          .Build();
+  DocumentProto kevin_call_log_doc2 =
+      DocumentBuilder()
+          .SetKey("namespace", "call_log_id4")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetSchema("CallLogSignalDoc")
+          .SetScore(5)
+          .AddDoubleProperty("rfsScore", 500, 800)
+          .AddStringProperty("personQualifiedId", "namespace#person_kevin_id")
+          .Build();
+  DocumentProto john_sms_log_doc1 =
+      DocumentBuilder()
+          .SetKey("namespace", "sms_log_id1")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetSchema("SmsLogSignalDoc")
+          .SetScore(5)
+          .AddDoubleProperty("rfsScore", 1000, 2000)
+          .AddStringProperty("personQualifiedId", "namespace#person_john_id")
+          .Build();
+  DocumentProto kevin_sms_log_doc1 =
+      DocumentBuilder()
+          .SetKey("namespace", "sms_log_id2")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetSchema("SmsLogSignalDoc")
+          .SetScore(5)
+          .AddDoubleProperty("rfsScore", 2000, 3000)
+          .AddStringProperty("personQualifiedId", "namespace#person_kevin_id")
+          .Build();
+
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(person_john_doc).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(person_kevin_doc).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(person_tim_doc).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(john_call_log_doc1).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(john_call_log_doc2).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(kevin_call_log_doc1).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(kevin_call_log_doc2).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(john_sms_log_doc1).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(kevin_sms_log_doc1).status(), ProtoIsOk());
+
+  // all people will be matched.
+  SearchSpecProto parent_search_spec;
+  parent_search_spec.add_schema_type_filters("Person");
+
+  // Child scoring spec: ranking by call logs and sms logs' rfsScore.
+  ScoringSpecProto child_scoring_spec = GetDefaultScoringSpec();
+  child_scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  child_scoring_spec.set_advanced_scoring_expression(
+      "sum(getScorableProperty(\"CallLogSignalDoc\", \"rfsScore\")) + "
+      "sum(getScorableProperty(\"SmsLogSignalDoc\", \"rfsScore\"))");
+  child_scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+
+  JoinSpecProto* join_spec = parent_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();
+  *nested_spec->mutable_scoring_spec() = child_scoring_spec;
+  nested_spec->mutable_search_spec()->add_schema_type_filters(
+      "CallLogSignalDoc");
+  nested_spec->mutable_search_spec()->add_schema_type_filters(
+      "SmsLogSignalDoc");
+
+  // Parent ScoringSpec
+  ScoringSpecProto parent_scoring_spec = GetDefaultScoringSpec();
+  parent_scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  parent_scoring_spec.set_advanced_scoring_expression(
+      "this.documentScore() + "
+      "sum(getScorableProperty(\"Person\", \"customizedScore\")) + "
+      "20 * sum(getScorableProperty(\"Person\", \"isStarred\")) + "
+      "maxOrDefault(this.childrenRankingSignals(), 0)");
+  AddSchemaTypeAliasMap(&parent_scoring_spec, "Person", {"Person"});
+  AddSchemaTypeAliasMap(&parent_scoring_spec, "CallLogSignalDoc",
+                        {"CallLogSignalDoc"});
+  AddSchemaTypeAliasMap(&parent_scoring_spec, "SmsLogSignalDoc",
+                        {"SmsLogSignalDoc"});
+  parent_scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  int person_john_expected_score =
+      10 + 30 + 20 * /*isStarred=*/1 +
+      std::max({100 + 200, 300 + 400, 1000 + 2000});
+  int person_kevin_expected_score =
+      20 + 40 + 20 * /*isStarred=*/0 +
+      std::max({300 + 400, 500 + 800, 2000 + 3000});
+  int person_tim_expected_score =
+      50 + 60 + 20 * /*isStarred=*/0 + /*child_ranking_signal=*/0;
+
+  SearchResultProto search_result_proto =
+      icing.Search(parent_search_spec, parent_scoring_spec,
+                   ResultSpecProto::default_instance());
+  EXPECT_THAT(search_result_proto.status(), ProtoIsOk());
+
+  // Verify that the search results are ranked as expected.
+  EXPECT_THAT(
+      GetUrisFromSearchResults(search_result_proto),
+      ElementsAre("person_kevin_id", "person_john_id", "person_tim_id"));
+  EXPECT_THAT(
+      GetScoresFromSearchResults(search_result_proto),
+      ElementsAre(person_kevin_expected_score, person_john_expected_score,
+                  person_tim_expected_score));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       JoinSearchWithRankingByScorableProperty_ScorablePropertyDisabled) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("CallLogSignalDoc")
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("personQualifiedId")
+                                   .SetDataTypeJoinableString(
+                                       JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                   .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("rfsScore")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+
+  // John has 2 call logs and 1 sms log to join.
+  DocumentProto person_john_doc =
+      DocumentBuilder()
+          .SetKey("namespace", "person_john_id")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetSchema("Person")
+          .SetScore(10)
+          .AddStringProperty("name", "John")
+          .Build();
+  DocumentProto john_call_log_doc1 =
+      DocumentBuilder()
+          .SetKey("namespace", "call_log_id1")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .SetSchema("CallLogSignalDoc")
+          .SetScore(5)
+          .AddDoubleProperty("rfsScore", 100, 200)
+          .AddStringProperty("personQualifiedId", "namespace#person_john_id")
+          .Build();
+
+  IcingSearchEngineOptions options = GetDefaultIcingOptions();
+  options.set_enable_scorable_properties(false);
+  IcingSearchEngine icing(options, GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(person_john_doc).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(john_call_log_doc1).status(), ProtoIsOk());
+
+  // all people will be matched.
+  SearchSpecProto parent_search_spec;
+  parent_search_spec.add_schema_type_filters("Person");
+
+  // Child scoring spec: ranking by call logs and sms logs' rfsScore.
+  ScoringSpecProto child_scoring_spec = GetDefaultScoringSpec();
+  child_scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  child_scoring_spec.set_advanced_scoring_expression(
+      "sum(getScorableProperty(\"CallLogSignalDoc\", \"rfsScore\"))");
+  AddSchemaTypeAliasMap(&child_scoring_spec, "Person", {"Person"});
+  AddSchemaTypeAliasMap(&child_scoring_spec, "CallLogSignalDoc",
+                        {"CallLogSignalDoc"});
+
+  JoinSpecProto* join_spec = parent_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();
+  *nested_spec->mutable_scoring_spec() = child_scoring_spec;
+  nested_spec->mutable_search_spec()->add_schema_type_filters(
+      "CallLogSignalDoc");
+
+  // Parent ScoringSpec
+  ScoringSpecProto parent_scoring_spec = GetDefaultScoringSpec();
+  parent_scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  parent_scoring_spec.set_advanced_scoring_expression(
+      "this.documentScore() + "
+      "maxOrDefault(this.childrenRankingSignals(), 0)");
+  AddSchemaTypeAliasMap(&parent_scoring_spec, "Person", {"Person"});
+  AddSchemaTypeAliasMap(&parent_scoring_spec, "CallLogSignalDoc",
+                        {"CallLogSignalDoc"});
+
+  SearchResultProto results =
+      icing.Search(parent_search_spec, parent_scoring_spec,
+                   ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoStatusIs(StatusProto::INVALID_ARGUMENT));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       SearchWithScorableProperty_DocumentsFromMultipleMatchedSchemaTypes) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("pkg1/db1/person")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("pkg2/db2/person")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  DocumentProto document0 = DocumentBuilder()
+                                .SetKey("icing", "person0")
+                                .SetSchema("pkg1/db1/person")
+                                .SetScore(10)
+                                .SetCreationTimestampMs(1)
+                                .AddDoubleProperty("income", 10000, 20000)
+                                .Build();
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("icing", "person1")
+                                .SetSchema("pkg2/db2/person")
+                                .SetScore(10)
+                                .SetCreationTimestampMs(1)
+                                .AddDoubleProperty("income", 30000, 40000)
+                                .Build();
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document0).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document1).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+
+  // Ranking by income.
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+  scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  scoring_spec.set_advanced_scoring_expression(
+      "sum(getScorableProperty(\"Person\", \"income\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "Person",
+                        {"pkg1/db1/person", "pkg2/db2/person"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  int expected_person0_score = 10000 + 20000;
+  int expected_person1_score = 30000 + 40000;
+
+  SearchResultProto search_result_proto = icing.Search(
+      search_spec, scoring_spec, ResultSpecProto::default_instance());
+  EXPECT_THAT(search_result_proto.status(), ProtoIsOk());
+
+  // Verify that the search results are ranked as expected.
+  EXPECT_THAT(GetUrisFromSearchResults(search_result_proto),
+              ElementsAre("person1", "person0"));
+  EXPECT_THAT(GetScoresFromSearchResults(search_result_proto),
+              ElementsAre(expected_person1_score, expected_person0_score));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       InvalidScoringSpecAliasMapWithEmptyAliasSchemaType) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+  AddSchemaTypeAliasMap(&scoring_spec, /*alias_schema_type=*/"", {"Person"});
+
+  SearchResultProto expected_search_result_proto;
+  expected_search_result_proto.mutable_status()->set_code(
+      StatusProto::INVALID_ARGUMENT);
+  expected_search_result_proto.mutable_status()->set_message(
+      "SchemaTypeAliasMapProto contains alias_schema_type with empty string");
+  EXPECT_THAT(
+      icing.Search(search_spec, scoring_spec,
+                   ResultSpecProto::default_instance()),
+      EqualsSearchResultIgnoreStatsAndScores(expected_search_result_proto));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       InvalidScoringSpecAliasMapWithEmptySchemaTypes) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+  AddSchemaTypeAliasMap(&scoring_spec, /*alias_schema_type=*/"Person", {});
+
+  SearchResultProto expected_search_result_proto;
+  expected_search_result_proto.mutable_status()->set_code(
+      StatusProto::INVALID_ARGUMENT);
+  expected_search_result_proto.mutable_status()->set_message(
+      "SchemaTypeAliasMapProto contains empty schema_types for "
+      "alias_schema_type: Person");
+  EXPECT_THAT(
+      icing.Search(search_spec, scoring_spec,
+                   ResultSpecProto::default_instance()),
+      EqualsSearchResultIgnoreStatsAndScores(expected_search_result_proto));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       InvalidScoringSpecAliasMapWithDuplicatedAliasSchemaTypes) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+  AddSchemaTypeAliasMap(&scoring_spec, /*alias_schema_type=*/"aliasPerson",
+                        {"PersonA"});
+  AddSchemaTypeAliasMap(&scoring_spec, /*alias_schema_type=*/"aliasPerson",
+                        {"PersonB"});
+
+  SearchResultProto expected_search_result_proto;
+  expected_search_result_proto.mutable_status()->set_code(
+      StatusProto::INVALID_ARGUMENT);
+  expected_search_result_proto.mutable_status()->set_message(
+      "SchemaTypeAliasMapProto contains multiple entries with the same "
+      "alias_schema_type: aliasPerson");
+  EXPECT_THAT(
+      icing.Search(search_spec, scoring_spec,
+                   ResultSpecProto::default_instance()),
+      EqualsSearchResultIgnoreStatsAndScores(expected_search_result_proto));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       SearchWithScorablePropertyWithUpdatePropertyAsScorable) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Person")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("name")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  DocumentProto document0 = DocumentBuilder()
+                                .SetKey("icing", "person0")
+                                .SetSchema("Person")
+                                .SetScore(10)
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("name", "John")
+                                .AddDoubleProperty("income", 20000)
+                                .Build();
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("icing", "person1")
+                                .SetSchema("Person")
+                                .SetScore(10)
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("name", "John")
+                                .AddDoubleProperty("income", 10000)
+                                .Build();
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document0).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document1).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+
+  AddSchemaTypeAliasMap(&scoring_spec, "Person", {"Person"});
+  scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  scoring_spec.set_advanced_scoring_expression(
+      "sum(getScorableProperty(\"Person\", \"income\"))");
+
+  SearchResultProto expected_search_result_proto;
+  expected_search_result_proto.mutable_status()->set_code(
+      StatusProto::INVALID_ARGUMENT);
+  expected_search_result_proto.mutable_status()->set_message(
+      "'income' is not defined as a scorable property under schema type 0");
+
+  SearchResultProto actual_search_result_proto = icing.Search(
+      search_spec, scoring_spec, ResultSpecProto::default_instance());
+  EXPECT_THAT(
+      actual_search_result_proto,
+      EqualsSearchResultIgnoreStatsAndScores(expected_search_result_proto));
+
+  // Update the schema to set Person.income as a scorable property.
+  SchemaProto new_schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Person")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("name")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  EXPECT_THAT(icing.SetSchema(new_schema).status(), ProtoIsOk());
+
+  actual_search_result_proto = icing.Search(
+      search_spec, scoring_spec, ResultSpecProto::default_instance());
+  EXPECT_THAT(actual_search_result_proto.status(), ProtoIsOk());
+
+  // Check the search results.
+  EXPECT_THAT(GetUrisFromSearchResults(actual_search_result_proto),
+              ElementsAre("person0", "person1"));
+  EXPECT_THAT(GetScoresFromSearchResults(actual_search_result_proto),
+              ElementsAre(20000, 10000));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       SearchWithScorablePropertyWithFlipPropertyScorability) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Person")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("name")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  DocumentProto document0 = DocumentBuilder()
+                                .SetKey("icing", "person0")
+                                .SetSchema("Person")
+                                .SetScore(10)
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("name", "John")
+                                .AddDoubleProperty("income", 20000)
+                                .Build();
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document0).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+
+  AddSchemaTypeAliasMap(&scoring_spec, "Person", {"Person"});
+  scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  scoring_spec.set_advanced_scoring_expression(
+      "sum(getScorableProperty(\"Person\", \"income\"))");
+
+  // Update the schema to set Person.income as not scorable. It would erase the
+  // data the scorable property cache.
+  schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Person")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("name")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_DISABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  SearchResultProto expected_search_result_proto;
+  expected_search_result_proto.mutable_status()->set_code(
+      StatusProto::INVALID_ARGUMENT);
+  expected_search_result_proto.mutable_status()->set_message(
+      "'income' is not defined as a scorable property under schema type 0");
+  SearchResultProto actual_search_result_proto = icing.Search(
+      search_spec, scoring_spec, ResultSpecProto::default_instance());
+  EXPECT_THAT(
+      actual_search_result_proto,
+      EqualsSearchResultIgnoreStatsAndScores(expected_search_result_proto));
+
+  // Update the schema to set Person.income as scorable again. It would
+  // re-populate the scorable property cache.
+  schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Person")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("name")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+
+  SearchResultProto search_result_proto = icing.Search(
+      search_spec, scoring_spec, ResultSpecProto::default_instance());
+  EXPECT_THAT(search_result_proto.status(), ProtoIsOk());
+
+  // Check the search results.
+  EXPECT_THAT(GetUrisFromSearchResults(search_result_proto),
+              ElementsAre("person0"));
+  EXPECT_THAT(GetScoresFromSearchResults(search_result_proto),
+              ElementsAre(20000));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       SearchWithScorablePropertyWithAddNewPropertyAsScorable) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("name")
+                  .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+  DocumentProto document0 = DocumentBuilder()
+                                .SetKey("icing", "person0")
+                                .SetSchema("Person")
+                                .SetScore(5)
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("name", "John")
+                                .Build();
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("icing", "person1")
+                                .SetSchema("Person")
+                                .SetScore(10)
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("name", "John")
+                                .Build();
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document0).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document1).status(), ProtoIsOk());
+
+  // Update the schema to add Person.income as a scorable property.
+  // It would populate the scorable property cache with the default values.
+  SchemaProto new_schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Person")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("name")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  EXPECT_THAT(icing.SetSchema(new_schema).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+
+  AddSchemaTypeAliasMap(&scoring_spec, "Person", {"Person"});
+  scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  scoring_spec.set_advanced_scoring_expression(
+      "this.documentScore() +sum(getScorableProperty(\"Person\", \"income\"))");
+
+  SearchResultProto search_result_proto = icing.Search(
+      search_spec, scoring_spec, ResultSpecProto::default_instance());
+  EXPECT_THAT(search_result_proto.status(), ProtoIsOk());
+
+  // Check the search results.
+  EXPECT_THAT(GetUrisFromSearchResults(search_result_proto),
+              ElementsAre("person1", "person0"));
+  EXPECT_THAT(GetScoresFromSearchResults(search_result_proto),
+              ElementsAre(10, 5));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       SearchWithScorablePropertyWithReorderScorableProperties) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Person")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("name")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("age")
+                          .SetDataType(PropertyConfigProto::DataType::INT64)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  DocumentProto document0 = DocumentBuilder()
+                                .SetKey("icing", "person0")
+                                .SetSchema("Person")
+                                .SetScore(5)
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("name", "John")
+                                .AddInt64Property("age", 20)
+                                .AddDoubleProperty("income", 10000, 20000)
+                                .Build();
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("icing", "person1")
+                                .SetSchema("Person")
+                                .SetScore(10)
+                                .SetCreationTimestampMs(1)
+                                .AddStringProperty("name", "Kevin")
+                                .AddInt64Property("age", 25)
+                                .AddDoubleProperty("income", 30000, 40000)
+                                .Build();
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document0).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document1).status(), ProtoIsOk());
+
+  // Update the schema change the order of the properties "age" and "income".
+  SchemaProto new_schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Person")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("name")
+                          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("age")
+                          .SetDataType(PropertyConfigProto::DataType::INT64)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  EXPECT_THAT(icing.SetSchema(new_schema).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+
+  AddSchemaTypeAliasMap(&scoring_spec, "Person", {"Person"});
+  scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  scoring_spec.set_advanced_scoring_expression(
+      "this.documentScore() + sum(getScorableProperty(\"Person\", \"income\")) "
+      "+ sum(getScorableProperty(\"Person\", \"age\"))");
+  int expected_score0 = 5 + (10000 + 20000) + 20;
+  int expected_score1 = 10 + (30000 + 40000) + 25;
+
+  SearchResultProto search_result_proto = icing.Search(
+      search_spec, scoring_spec, ResultSpecProto::default_instance());
+  EXPECT_THAT(search_result_proto.status(), ProtoIsOk());
+
+  // Check the search results.
+  EXPECT_THAT(GetUrisFromSearchResults(search_result_proto),
+              ElementsAre("person1", "person0"));
+  EXPECT_THAT(GetScoresFromSearchResults(search_result_proto),
+              ElementsAre(expected_score1, expected_score0));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       SearchWithScorablePropertyWithUpdatePropertyAsScorableInNestedSchema) {
+  SchemaProto schema_proto =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Person")
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("id")
+                                   .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                   .SetCardinality(CARDINALITY_REPEATED))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_DISABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("subject")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(
+                           PropertyConfigBuilder()
+                               .SetName("sender")
+                               .SetDataTypeDocument(
+                                   "Person", /*index_nested_properties=*/true)
+                               .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(
+                           PropertyConfigBuilder()
+                               .SetName("receiver")
+                               .SetDataTypeDocument(
+                                   "Person", /*index_nested_properties=*/true)
+                               .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  DocumentProto document =
+      DocumentBuilder()
+          .SetKey("namespace", "email_id0")
+          .SetSchema("Email")
+          .AddDocumentProperty("receiver",
+                               DocumentBuilder()
+                                   .SetKey("namespace", "receiver0")
+                                   .SetSchema("Person")
+                                   .AddDoubleProperty("income", 10, 20)
+                                   .Build(),
+                               DocumentBuilder()
+                                   .SetKey("namespace", "receiver1")
+                                   .SetSchema("Person")
+                                   .AddDoubleProperty("income", 11, 12)
+                                   .Build())
+          .AddDocumentProperty("sender",
+                               DocumentBuilder()
+                                   .SetKey("namespace", "sender0")
+                                   .SetSchema("Person")
+                                   .AddDoubleProperty("income", 21, 22)
+                                   .Build())
+          .Build();
+
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  EXPECT_THAT(icing.Initialize().status(), ProtoIsOk());
+  EXPECT_THAT(icing.SetSchema(schema_proto).status(), ProtoIsOk());
+  EXPECT_THAT(icing.Put(document).status(), ProtoIsOk());
+
+  // Update the 'Person' schema by setting Person.income as scorable.
+  // It would trigger the re-generation of the scorable property cache for the
+  // the schema 'Email', as it is a parent schema of 'Person'.
+  schema_proto =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("Person")
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("id")
+                                   .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                   .SetCardinality(CARDINALITY_REPEATED))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("Email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("subject")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(
+                           PropertyConfigBuilder()
+                               .SetName("sender")
+                               .SetDataTypeDocument(
+                                   "Person", /*index_nested_properties=*/true)
+                               .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(
+                           PropertyConfigBuilder()
+                               .SetName("receiver")
+                               .SetDataTypeDocument(
+                                   "Person", /*index_nested_properties=*/true)
+                               .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+  EXPECT_THAT(icing.SetSchema(schema_proto).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  ScoringSpecProto scoring_spec = GetDefaultScoringSpec();
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+
+  AddSchemaTypeAliasMap(&scoring_spec, "Email", {"Email"});
+  scoring_spec.set_rank_by(
+      ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
+  scoring_spec.set_advanced_scoring_expression(
+      "sum(getScorableProperty(\"Email\", \"sender.income\")) + "
+      "sum(getScorableProperty(\"Email\", \"receiver.income\"))");
+  int expected_score = (21 + 22) + (10 + 20 + 11 + 12);
+
+  SearchResultProto search_result_proto = icing.Search(
+      search_spec, scoring_spec, ResultSpecProto::default_instance());
+  EXPECT_THAT(search_result_proto.status(), ProtoIsOk());
+
+  EXPECT_THAT(GetUrisFromSearchResults(search_result_proto),
+              ElementsAre("email_id0"));
+  EXPECT_THAT(GetScoresFromSearchResults(search_result_proto),
+              ElementsAre(expected_score));
+}
+
+TEST_F(IcingSearchEngineSearchTest, MatchScoreExpression) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  // Add three documents with different document scores and "body" values.
+  DocumentProto document_one =
+      DocumentBuilder()
+          .SetKey("namespace", "uri1")
+          .SetSchema("Message")
+          .AddStringProperty("body", "foo")
+          .SetScore(2)
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto document_two =
+      DocumentBuilder()
+          .SetKey("namespace", "uri2")
+          .SetSchema("Message")
+          .AddStringProperty("body", "foo")
+          .SetScore(3)
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto document_three =
+      DocumentBuilder()
+          .SetKey("namespace", "uri3")
+          .SetSchema("Message")
+          .AddStringProperty("body", "bar")
+          .SetScore(4)
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+
+  ASSERT_THAT(icing.Put(document_one).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document_two).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(document_three).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(document_three));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(document_two));
+
+  // Get documents with a document score in [3, 4] AND a "foo" body, which
+  // matches document 2.
+  search_spec.set_query(
+      "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(document_two));
+
+  // Get documents with a document score in [3, 4] OR a "foo" body, which
+  // matches all documents.
+  search_spec.set_query(
+      "foo OR matchScoreExpression(\"this.documentScore()\", 3, 4)");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status(), ProtoIsOk());
+  EXPECT_THAT(results.results(), SizeIs(3));
+  EXPECT_THAT(results.results(0).document(), EqualsProto(document_three));
+  EXPECT_THAT(results.results(1).document(), EqualsProto(document_two));
+  EXPECT_THAT(results.results(2).document(), EqualsProto(document_one));
+}
+
+TEST_F(IcingSearchEngineSearchTest, MatchScoreExpressionNotSupportedFunctions) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  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(\"len(this.childrenRankingSignals())\", 0, 1)");
+  SearchResultProto results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                                           ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status().message(),
+              HasSubstr("childrenRankingSignals must only be used with join"));
+  EXPECT_THAT(results.status().message(),
+              HasSubstr("matchScoreExpression: Failed to handle"));
+
+  search_spec.set_query(
+      "matchScoreExpression(\"sum(this.matchedSemanticScores("
+      "getEmbeddingParameter(0)))\", 0, 1)");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status().message(),
+              HasSubstr("matchedSemanticScores function is not available"));
+  EXPECT_THAT(results.status().message(),
+              HasSubstr("matchScoreExpression: Failed to handle"));
+
+  search_spec.set_query(
+      "matchScoreExpression(\"sum(this.propertyWeights())\", 0, 1)");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status().message(),
+              HasSubstr("propertyWeights function is not available"));
+  EXPECT_THAT(results.status().message(),
+              HasSubstr("matchScoreExpression: Failed to handle"));
+
+  search_spec.set_query(
+      "matchScoreExpression(\"this.relevanceScore()\", 0, 1)");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(),
+                         ResultSpecProto::default_instance());
+  EXPECT_THAT(results.status().message(),
+              HasSubstr("relevanceScore function is not available"));
+  EXPECT_THAT(results.status().message(),
+              HasSubstr("matchScoreExpression: Failed to handle"));
+}
+
+TEST_F(IcingSearchEngineSearchTest, MatchScoreExpressionNotEnabled) {
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(CreateMessageSchema()).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  search_spec.set_term_match_type(TermMatchType::EXACT_ONLY);
+  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().message(),
+              HasSubstr("Attempted use of unenabled feature"));
+}
+
+TEST_F(IcingSearchEngineSearchTest, JoinWithMatchScoreExpression) {
+  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("personQualifiedId")
+                                        .SetDataTypeJoinableString(
+                                            JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  // person1 has children email1 and email2.
+  DocumentProto person1 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "person1")
+          .SetSchema("Person")
+          .AddStringProperty("name", "foo")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  // person2 has a single child email3
+  DocumentProto person2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "person2")
+          .SetSchema("Person")
+          .AddStringProperty("name", "bar")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  // person3 has no child.
+  DocumentProto person3 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "person3")
+          .SetSchema("Person")
+          .AddStringProperty("name", "foo")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+
+  DocumentProto email1 =
+      DocumentBuilder()
+          .SetKey("namespace", "email1")
+          .SetSchema("Email")
+          .AddStringProperty("subject", "test subject 1")
+          .AddStringProperty("personQualifiedId", "pkg$db/namespace#person1")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto email2 =
+      DocumentBuilder()
+          .SetKey("namespace", "email2")
+          .SetSchema("Email")
+          .AddStringProperty("subject", "test subject 2")
+          .AddStringProperty("personQualifiedId", "pkg$db/namespace#person1")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto email3 =
+      DocumentBuilder()
+          .SetKey("namespace", "email3")
+          .SetSchema("Email")
+          .AddStringProperty("subject", "test subject 3")
+          .AddStringProperty("personQualifiedId", "pkg$db/namespace#person2")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+
+  IcingSearchEngine icing(GetDefaultIcingOptions(), 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(person3).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email1).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email2).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email3).status(), ProtoIsOk());
+
+  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));
+
+  // 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();
+
+  // Get all parent documents that have at least 1 join children, which should
+  // match person1 and person2, since person3 has no children.
+  search_spec.set_query(
+      "matchScoreExpression(\"len(this.childrenRankingSignals())\", 1)");
+  ResultSpecProto result_spec;
+  result_spec.set_max_joined_children_per_parent_to_return(
+      std::numeric_limits<int32_t>::max());
+  SearchResultProto results =
+      icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
+  ASSERT_THAT(results.status(), ProtoIsOk());
+  ASSERT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document().uri(), Eq("person2"));
+  EXPECT_THAT(results.results(0).joined_results_size(), Eq(1));
+  EXPECT_THAT(results.results(0).joined_results(0).document().uri(),
+              Eq("email3"));
+  EXPECT_THAT(results.results(1).document().uri(), Eq("person1"));
+  EXPECT_THAT(results.results(1).joined_results_size(), Eq(2));
+  EXPECT_THAT(results.results(1).joined_results(0).document().uri(),
+              Eq("email2"));
+  EXPECT_THAT(results.results(1).joined_results(1).document().uri(),
+              Eq("email1"));
+
+  // Get all parent documents with "foo" name and join with email documents,
+  // which should match person1 and person3.
+  search_spec.set_query("foo");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
+  ASSERT_THAT(results.status(), ProtoIsOk());
+  ASSERT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document().uri(), Eq("person3"));
+  EXPECT_THAT(results.results(0).joined_results_size(), Eq(0));
+  EXPECT_THAT(results.results(1).document().uri(), Eq("person1"));
+  EXPECT_THAT(results.results(1).joined_results_size(), Eq(2));
+  EXPECT_THAT(results.results(1).joined_results(0).document().uri(),
+              Eq("email2"));
+  EXPECT_THAT(results.results(1).joined_results(1).document().uri(),
+              Eq("email1"));
+
+  // Get all parent documents with "foo" name OR with at least 1 join children,
+  // which should match all 3 documents.
+  // This is analogous to a SQL "full outer join."
+  search_spec.set_query(
+      "foo OR matchScoreExpression(\"len(this.childrenRankingSignals())\", 1)");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
+  ASSERT_THAT(results.status(), ProtoIsOk());
+  ASSERT_THAT(results.results(), SizeIs(3));
+  // Person3 is returned because it has "foo" matched, even though it has no
+  // join children.
+  EXPECT_THAT(results.results(0).document().uri(), Eq("person3"));
+  EXPECT_THAT(results.results(0).joined_results_size(), Eq(0));
+  EXPECT_THAT(results.results(1).document().uri(), Eq("person2"));
+  EXPECT_THAT(results.results(1).joined_results_size(), Eq(1));
+  EXPECT_THAT(results.results(1).joined_results(0).document().uri(),
+              Eq("email3"));
+  EXPECT_THAT(results.results(2).document().uri(), Eq("person1"));
+  EXPECT_THAT(results.results(2).joined_results_size(), Eq(2));
+  EXPECT_THAT(results.results(2).joined_results(0).document().uri(),
+              Eq("email2"));
+  EXPECT_THAT(results.results(2).joined_results(1).document().uri(),
+              Eq("email1"));
+
+  // Get all parent documents with "foo" name AND with at least 1 join children,
+  // which should only match person1.
+  // This is analogous to a SQL "inner join."
+  search_spec.set_query(
+      "foo AND "
+      "matchScoreExpression(\"len(this.childrenRankingSignals())\", 1)");
+  results = icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
+  ASSERT_THAT(results.status(), ProtoIsOk());
+  ASSERT_THAT(results.results(), SizeIs(1));
+  EXPECT_THAT(results.results(0).document().uri(), Eq("person1"));
+  EXPECT_THAT(results.results(0).joined_results_size(), Eq(2));
+  EXPECT_THAT(results.results(0).joined_results(0).document().uri(),
+              Eq("email2"));
+  EXPECT_THAT(results.results(0).joined_results(1).document().uri(),
+              Eq("email1"));
+}
+
+TEST_F(IcingSearchEngineSearchTest,
+       HybridEmbeddingSearchUsingJoinWithMatchScoreExpression) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Email").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("body")
+                  .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_REPEATED)))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("EmbeddingAttachment")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("embedding")
+                                        .SetDataTypeVector(
+                                            EMBEDDING_INDEXING_LINEAR_SEARCH)
+                                        .SetCardinality(CARDINALITY_REPEATED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("emailQualifiedId")
+                                        .SetDataTypeJoinableString(
+                                            JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  PropertyProto::VectorProto query_embedding =
+      CreateVector("my_model", {1, 1, 1});
+  // Create email1 with body "foo", and with an embedding attachment of score
+  // 0.1 + 0.2 + 0.3 = 0.6 by join.
+  DocumentProto email1 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email1")
+          .SetSchema("Email")
+          .AddStringProperty("body", "foo")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto embedding1 =
+      DocumentBuilder()
+          .SetKey("namespace", "embedding1")
+          .SetSchema("EmbeddingAttachment")
+          .AddVectorProperty("embedding",
+                             CreateVector("my_model", {0.1, 0.2, 0.3}))
+          .AddStringProperty("emailQualifiedId", "pkg$db/namespace#email1")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+
+  // Create email2 with body "bar", and with an embedding attachment of score
+  // 0.2 + 0.3 + 0.4 = 0.9 by join.
+  DocumentProto email2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email2")
+          .SetSchema("Email")
+          .AddStringProperty("body", "bar")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto embedding2 =
+      DocumentBuilder()
+          .SetKey("namespace", "embedding2")
+          .SetSchema("EmbeddingAttachment")
+          .AddVectorProperty("embedding",
+                             CreateVector("my_model", {0.2, 0.3, 0.4}))
+          .AddStringProperty("emailQualifiedId", "pkg$db/namespace#email2")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+
+  // Create email3 with body "baz", and with an embedding attachment of score
+  // 0.1 + 0.2 + 0.3 = 0.6 by join.
+  DocumentProto email3 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email3")
+          .SetSchema("Email")
+          .AddStringProperty("body", "baz")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+  DocumentProto embedding3 =
+      DocumentBuilder()
+          .SetKey("namespace", "embedding3")
+          .SetSchema("EmbeddingAttachment")
+          .AddVectorProperty("embedding",
+                             CreateVector("my_model", {0.1, 0.2, 0.3}))
+          .AddStringProperty("emailQualifiedId", "pkg$db/namespace#email3")
+          .SetCreationTimestampMs(kDefaultCreationTimestampMs)
+          .Build();
+
+  IcingSearchEngine icing(GetDefaultIcingOptions(), GetTestJniCache());
+  ASSERT_THAT(icing.Initialize().status(), ProtoIsOk());
+  ASSERT_THAT(icing.SetSchema(schema).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email1).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(embedding1).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email2).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(embedding2).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(email3).status(), ProtoIsOk());
+  ASSERT_THAT(icing.Put(embedding3).status(), ProtoIsOk());
+
+  SearchSpecProto search_spec;
+  search_spec.add_enabled_features(
+      std::string(kMatchScoreExpressionFunctionFeature));
+  search_spec.add_enabled_features(
+      std::string(kListFilterQueryLanguageFeature));
+
+  // 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("emailQualifiedId");
+  JoinSpecProto::NestedSpecProto* nested_spec =
+      join_spec->mutable_nested_spec();
+  *nested_spec->mutable_scoring_spec() = GetDefaultScoringSpec();
+  *nested_spec->mutable_result_spec() = ResultSpecProto::default_instance();
+  SearchSpecProto* nested_search_spec = nested_spec->mutable_search_spec();
+  nested_search_spec->add_enabled_features(
+      std::string(kListFilterQueryLanguageFeature));
+
+  // Get all parent documents with "foo" OR an embedding attachment of
+  // score >= 0.8, which should match email1 (because it has "foo") and email2
+  // (because it has an embedding attachment of score 0.9). This is analogous to
+  // a SQL "full outer join."
+  search_spec.set_term_match_type(TermMatchType::EXACT_ONLY);
+  search_spec.set_query(
+      "foo OR matchScoreExpression(\"len(this.childrenRankingSignals())\", 1)");
+  *nested_search_spec->add_embedding_query_vectors() = query_embedding;
+  nested_search_spec->set_embedding_query_metric_type(
+      SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT);
+  nested_search_spec->set_query(
+      "semanticSearch(getEmbeddingParameter(0), 0.8)");
+
+  ResultSpecProto result_spec;
+  result_spec.set_max_joined_children_per_parent_to_return(
+      std::numeric_limits<int32_t>::max());
+  SearchResultProto results =
+      icing.Search(search_spec, GetDefaultScoringSpec(), result_spec);
+  ASSERT_THAT(results.status(), ProtoIsOk());
+  ASSERT_THAT(results.results(), SizeIs(2));
+  EXPECT_THAT(results.results(0).document().uri(), Eq("email2"));
+  EXPECT_THAT(results.results(0).joined_results_size(), Eq(1));
+  EXPECT_THAT(results.results(0).joined_results(0).document().uri(),
+              Eq("embedding2"));
+  EXPECT_THAT(results.results(1).document().uri(), Eq("email1"));
+  // email1's embedding attachment is not matched, so it is not returned.
+  EXPECT_THAT(results.results(1).joined_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 7344241..1f814aa 100644
--- a/icing/icing-search-engine_suggest_test.cc
+++ b/icing/icing-search-engine_suggest_test.cc
@@ -46,10 +46,10 @@
 #include "icing/schema-builder.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/jni-test-helpers.h"
 #include "icing/testing/test-data.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/icu-data-file-helper.h"
 
 namespace icing {
 namespace lib {
@@ -92,7 +92,7 @@
       std::string icu_data_file_path =
           GetTestFilePath("icing/icu.dat");
       ICING_ASSERT_OK(
-          icu_data_file_helper::SetUpICUDataFile(icu_data_file_path));
+          icu_data_file_helper::SetUpIcuDataFile(icu_data_file_path));
     }
     filesystem_.CreateDirectoryRecursively(GetTestBaseDir().c_str());
   }
diff --git a/icing/icing-search-engine_test.cc b/icing/icing-search-engine_test.cc
index 9ef1dfc..14b9b7d 100644
--- a/icing/icing-search-engine_test.cc
+++ b/icing/icing-search-engine_test.cc
@@ -48,10 +48,10 @@
 #include "icing/schema-builder.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/jni-test-helpers.h"
 #include "icing/testing/test-data.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/icu-data-file-helper.h"
 
 namespace icing {
 namespace lib {
@@ -99,7 +99,7 @@
       std::string icu_data_file_path =
           GetTestFilePath("icing/icu.dat");
       ICING_ASSERT_OK(
-          icu_data_file_helper::SetUpICUDataFile(icu_data_file_path));
+          icu_data_file_helper::SetUpIcuDataFile(icu_data_file_path));
     }
     filesystem_.CreateDirectoryRecursively(GetTestBaseDir().c_str());
   }
diff --git a/icing/index/data-indexing-handler.h b/icing/index/data-indexing-handler.h
index 16a1796..317eca6 100644
--- a/icing/index/data-indexing-handler.h
+++ b/icing/index/data-indexing-handler.h
@@ -32,16 +32,25 @@
   virtual ~DataIndexingHandler() = default;
 
   // Handles the indexing process: add data into the specific type index (e.g.
-  // term index, integer index, qualified id type joinable index) for all
-  // contents in the corresponding type of data in tokenized_document.
-  // For example, IntegerSectionIndexingHandler::Handle should add data into
-  // integer index for all contents in tokenized_document.integer_sections.
+  // term index, integer index, qualified id type joinable index, embedding
+  // index) for all contents in the corresponding type of data in
+  // tokenized_document. For example, IntegerSectionIndexingHandler::Handle
+  // should add data into integer index for all contents in
+  // tokenized_document.integer_sections.
+  //
+  // old_document_id is provided. If valid, then it means the document with
+  // the same (namespace, uri) exists previously, and it is updated with new
+  // contents at this round. Each indexing handler should decide whether
+  // migrating existing data from old_document_id to (new) document_id according
+  // to each index's data logic.
   //
   // Also it should handle last added DocumentId properly (based on
   // recovery_mode_) to avoid adding previously indexed documents.
   //
   // tokenized_document: document object with different types of tokenized data.
   // document_id:        id of the document.
+  // old_document_id:    id of the document before the update. If it is a new
+  //                     document, then it will be kInvalidDocumentId.
   // recovery_mode:      decides how to handle document_id <=
   //                     last_added_document_id. If in recovery_mode, then
   //                     Handle() will simply return OK immediately. Otherwise,
@@ -57,7 +66,8 @@
   //   - Any other errors. It depends on each implementation.
   virtual libtextclassifier3::Status Handle(
       const TokenizedDocument& tokenized_document, DocumentId document_id,
-      bool recovery_mode, PutDocumentStatsProto* put_document_stats) = 0;
+      DocumentId old_document_id, bool recovery_mode,
+      PutDocumentStatsProto* put_document_stats) = 0;
 
  protected:
   const Clock& clock_;  // Does not own.
diff --git a/icing/index/embed/doc-hit-info-iterator-embedding.cc b/icing/index/embed/doc-hit-info-iterator-embedding.cc
index 5a6e547..1fa5fac 100644
--- a/icing/index/embed/doc-hit-info-iterator-embedding.cc
+++ b/icing/index/embed/doc-hit-info-iterator-embedding.cc
@@ -14,6 +14,7 @@
 
 #include "icing/index/embed/doc-hit-info-iterator-embedding.h"
 
+#include <cstdint>
 #include <memory>
 #include <string_view>
 #include <utility>
@@ -29,10 +30,12 @@
 #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/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"
 
 namespace icing {
@@ -44,10 +47,13 @@
     SearchSpecProto::EmbeddingQueryMetricType::Code metric_type,
     double score_low, double score_high,
     EmbeddingQueryResults::EmbeddingQueryScoreMap* score_map,
-    const EmbeddingIndex* embedding_index) {
+    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(score_map);
+  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);
@@ -69,7 +75,8 @@
   return std::unique_ptr<DocHitInfoIteratorEmbedding>(
       new DocHitInfoIteratorEmbedding(
           query, metric_type, std::move(embedding_scorer), score_low,
-          score_high, score_map, embedding_index, std::move(pl_accessor)));
+          score_high, score_map, embedding_index, std::move(pl_accessor),
+          document_store, schema_store, current_time_ms));
 }
 
 libtextclassifier3::StatusOr<const EmbeddingHit*>
@@ -89,6 +96,14 @@
     doc_hit_info_.set_document_id(embedding_hit.basic_hit().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.
+      current_allowed_sections_mask_ = kSectionIdMaskNone;
+    }
   } else if (doc_hit_info_.document_id() !=
              embedding_hit.basic_hit().document_id()) {
     return nullptr;
@@ -105,6 +120,7 @@
   }
 
   doc_hit_info_ = DocHitInfo(kInvalidDocumentId, kSectionIdMaskNone);
+  schema_type_id_ = kInvalidSchemaTypeId;
   std::vector<double>* matched_scores = nullptr;
   current_allowed_sections_mask_ = kSectionIdMaskAll;
   while (true) {
@@ -121,15 +137,19 @@
       continue;
     }
 
-    // Calculate the semantic score.
-    int dimension = query_.values_size();
+    // The schema type id is guaranteed to be valid here. Otherwise,
+    // current_allowed_sections_mask_ should be assigned to kSectionIdMaskNone
+    // by AdvanceToNextEmbeddingHit, and the embedding hit should have been
+    // skipped above.
     ICING_ASSIGN_OR_RETURN(
-        const float* vector,
-        embedding_index_.GetEmbeddingVector(*embedding_hit, dimension));
-    double semantic_score =
-        embedding_scorer_->Score(dimension,
-                                 /*v1=*/query_.values().data(),
-                                 /*v2=*/vector);
+        EmbeddingIndexingConfig::QuantizationType::Code quantization_type,
+        schema_store_.GetQuantizationType(
+            schema_type_id_, embedding_hit->basic_hit().section_id()));
+    // Calculate the semantic score.
+    ICING_ASSIGN_OR_RETURN(
+        float semantic_score,
+        embedding_index_.ScoreEmbeddingHit(*embedding_scorer_, query_,
+                                           *embedding_hit, quantization_type));
 
     // If the semantic score is within the desired score range, update
     // doc_hit_info_ and score_map_.
diff --git a/icing/index/embed/doc-hit-info-iterator-embedding.h b/icing/index/embed/doc-hit-info-iterator-embedding.h
index ae126c8..3076cd7 100644
--- a/icing/index/embed/doc-hit-info-iterator-embedding.h
+++ b/icing/index/embed/doc-hit-info-iterator-embedding.h
@@ -15,6 +15,7 @@
 #ifndef ICING_INDEX_EMBED_DOC_HIT_INFO_ITERATOR_EMBEDDING_H_
 #define ICING_INDEX_EMBED_DOC_HIT_INFO_ITERATOR_EMBEDDING_H_
 
+#include <cstdint>
 #include <memory>
 #include <string>
 #include <string_view>
@@ -32,7 +33,10 @@
 #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-store.h"
 
 namespace icing {
 namespace lib {
@@ -59,7 +63,9 @@
          SearchSpecProto::EmbeddingQueryMetricType::Code metric_type,
          double score_low, double score_high,
          EmbeddingQueryResults::EmbeddingQueryScoreMap* score_map,
-         const EmbeddingIndex* embedding_index);
+         const EmbeddingIndex* embedding_index,
+         const DocumentStore* document_store, const SchemaStore* schema_store,
+         int64_t current_time_ms);
 
   libtextclassifier3::Status Advance() override;
 
@@ -92,7 +98,9 @@
       double score_high,
       EmbeddingQueryResults::EmbeddingQueryScoreMap* score_map,
       const EmbeddingIndex* embedding_index,
-      std::unique_ptr<PostingListEmbeddingHitAccessor> posting_list_accessor)
+      std::unique_ptr<PostingListEmbeddingHitAccessor> posting_list_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)),
@@ -104,6 +112,10 @@
         cached_embedding_hits_idx_(0),
         current_allowed_sections_mask_(kSectionIdMaskAll),
         no_more_hit_(false),
+        schema_type_id_(kInvalidSchemaTypeId),
+        document_store_(*document_store),
+        schema_store_(*schema_store),
+        current_time_ms_(current_time_ms),
         num_advance_calls_(0) {}
 
   // Advance to the next embedding hit of the current document. If the current
@@ -152,7 +164,11 @@
   int cached_embedding_hits_idx_;
   SectionIdMask current_allowed_sections_mask_;
   bool no_more_hit_;
+  SchemaTypeId schema_type_id_;  // The schema type id for the current document.
 
+  const DocumentStore& document_store_;
+  const SchemaStore& schema_store_;
+  int64_t current_time_ms_;
   int num_advance_calls_;
 };
 
diff --git a/icing/index/embed/embedding-index.cc b/icing/index/embed/embedding-index.cc
index 0f8ad53..663f53b 100644
--- a/icing/index/embed/embedding-index.cc
+++ b/icing/index/embed/embedding-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/destructible-directory.h"
 #include "icing/file/file-backed-vector.h"
 #include "icing/file/filesystem.h"
@@ -34,11 +35,17 @@
 #include "icing/file/posting_list/flash-index-storage.h"
 #include "icing/file/posting_list/posting-list-identifier.h"
 #include "icing/index/embed/embedding-hit.h"
+#include "icing/index/embed/embedding-scorer.h"
 #include "icing/index/embed/posting-list-embedding-hit-accessor.h"
+#include "icing/index/embed/quantizer.h"
 #include "icing/index/hit/hit.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/dynamic-trie-key-mapper.h"
 #include "icing/store/key-mapper.h"
+#include "icing/util/clock.h"
 #include "icing/util/crc32.h"
 #include "icing/util/encode-util.h"
 #include "icing/util/logging.h"
@@ -72,6 +79,11 @@
   return absl_ports::StrCat(working_path, "/embedding_vectors");
 }
 
+std::string GetQuantizedEmbeddingVectorsFilePath(
+    std::string_view working_path) {
+  return absl_ports::StrCat(working_path, "/quantized_embedding_vectors");
+}
+
 // An injective function that maps the ordered pair (dimension, model_signature)
 // to a string, which is used to form a key for embedding_posting_list_mapper_.
 std::string GetPostingListKey(uint32_t dimension,
@@ -94,17 +106,30 @@
 }
 
 std::string GetPostingListKey(const PropertyProto::VectorProto& vector) {
-  return GetPostingListKey(vector.values_size(), vector.model_signature());
+  return GetPostingListKey(vector.values().size(), vector.model_signature());
+}
+
+libtextclassifier3::StatusOr<Quantizer> CreateQuantizer(
+    const PropertyProto::VectorProto& vector) {
+  if (vector.values().empty()) {
+    return absl_ports::InvalidArgumentError("Vector dimension is 0");
+  }
+  auto minmax_pair =
+      std::minmax_element(vector.values().begin(), vector.values().end());
+  return Quantizer::Create(*minmax_pair.first, *minmax_pair.second);
 }
 
 }  // namespace
 
 libtextclassifier3::StatusOr<std::unique_ptr<EmbeddingIndex>>
-EmbeddingIndex::Create(const Filesystem* filesystem, std::string working_path) {
+EmbeddingIndex::Create(const Filesystem* filesystem, std::string working_path,
+                       const Clock* clock, const FeatureFlags* feature_flags) {
   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)));
+  std::unique_ptr<EmbeddingIndex> index =
+      std::unique_ptr<EmbeddingIndex>(new EmbeddingIndex(
+          *filesystem, std::move(working_path), clock, feature_flags));
   ICING_RETURN_IF_ERROR(index->Initialize());
   return index;
 }
@@ -133,6 +158,12 @@
           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));
+
   return libtextclassifier3::Status::OK;
 }
 
@@ -193,6 +224,7 @@
   flash_index_storage_.reset();
   embedding_posting_list_mapper_.reset();
   embedding_vectors_.reset();
+  quantized_embedding_vectors_.reset();
   if (filesystem_.DirectoryExists(working_path_.c_str())) {
     ICING_RETURN_IF_ERROR(Discard(filesystem_, working_path_));
   }
@@ -218,26 +250,56 @@
       posting_list_id);
 }
 
+libtextclassifier3::StatusOr<uint32_t> EmbeddingIndex::AppendEmbeddingVector(
+    const PropertyProto::VectorProto& vector,
+    EmbeddingIndexingConfig::QuantizationType::Code quantization_type) {
+  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>::MutableArrayView mutable_arr,
+        embedding_vectors_->Allocate(dimension));
+    mutable_arr.SetArray(/*idx=*/0, vector.values().data(), dimension);
+  } else {
+    ICING_ASSIGN_OR_RETURN(Quantizer quantizer, CreateQuantizer(vector));
+    // Quantize the vector
+    std::vector<uint8_t> quantized_values;
+    quantized_values.reserve(vector.values().size());
+    for (float value : vector.values()) {
+      quantized_values.push_back(quantizer.Quantize(value));
+    }
+
+    // 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));
+    mutable_arr.SetArray(/*idx=*/0, reinterpret_cast<char*>(&quantizer),
+                         sizeof(Quantizer));
+    mutable_arr.SetArray(/*idx=*/sizeof(Quantizer),
+                         reinterpret_cast<char*>(quantized_values.data()),
+                         dimension);
+  }
+  return location;
+}
+
 libtextclassifier3::Status EmbeddingIndex::BufferEmbedding(
-    const BasicHit& basic_hit, const PropertyProto::VectorProto& vector) {
-  if (vector.values_size() == 0) {
+    const BasicHit& basic_hit, const PropertyProto::VectorProto& vector,
+    EmbeddingIndexingConfig::QuantizationType::Code quantization_type) {
+  if (vector.values().empty()) {
     return absl_ports::InvalidArgumentError("Vector dimension is 0");
   }
   ICING_RETURN_IF_ERROR(MarkIndexNonEmpty());
 
-  uint32_t location = embedding_vectors_->num_elements();
-  uint32_t dimension = vector.values_size();
   std::string key = GetPostingListKey(vector);
+  ICING_ASSIGN_OR_RETURN(uint32_t location,
+                         AppendEmbeddingVector(vector, quantization_type));
 
   // Buffer the embedding hit.
   pending_embedding_hits_.push_back(
       {std::move(key), EmbeddingHit(basic_hit, location)});
-
-  // Put vector
-  ICING_ASSIGN_OR_RETURN(FileBackedVector<float>::MutableArrayView mutable_arr,
-                         embedding_vectors_->Allocate(dimension));
-  mutable_arr.SetArray(/*idx=*/0, vector.values().data(), dimension);
-
   return libtextclassifier3::Status::OK;
 }
 
@@ -303,13 +365,45 @@
   return libtextclassifier3::Status::OK;
 }
 
+libtextclassifier3::StatusOr<uint32_t> EmbeddingIndex::TransferEmbeddingVector(
+    const EmbeddingHit& old_hit, uint32_t dimension,
+    EmbeddingIndexingConfig::QuantizationType::Code quantization_type,
+    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();
+
+    // 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.
+    ICING_ASSIGN_OR_RETURN(FileBackedVector<char>::MutableArrayView mutable_arr,
+                           new_index->quantized_embedding_vectors_->Allocate(
+                               sizeof(Quantizer) + dimension));
+    mutable_arr.SetArray(/*idx=*/0, old_data, sizeof(Quantizer) + dimension);
+  }
+  return new_location;
+}
+
 libtextclassifier3::Status EmbeddingIndex::TransferIndex(
+    const DocumentStore& document_store, const SchemaStore& schema_store,
     const std::vector<DocumentId>& document_id_old_to_new,
     EmbeddingIndex* new_index) const {
   if (is_empty()) {
     return absl_ports::FailedPreconditionError("EmbeddingIndex is empty");
   }
 
+  const int64_t current_time_ms = clock_.GetSystemTimeMilliseconds();
   std::unique_ptr<KeyMapper<PostingListIdentifier>::Iterator> itr =
       embedding_posting_list_mapper_->GetIterator();
   while (itr->Advance()) {
@@ -330,6 +424,8 @@
         PostingListEmbeddingHitAccessor::CreateFromExisting(
             flash_index_storage_.get(), posting_list_hit_serializer_.get(),
             /*existing_posting_list_id=*/itr->GetValue()));
+    DocumentId last_new_document_id = kInvalidDocumentId;
+    SchemaTypeId schema_type_id = kInvalidSchemaTypeId;
     while (true) {
       ICING_ASSIGN_OR_RETURN(std::vector<EmbeddingHit> batch,
                              old_pl_accessor->GetNextHitsBatch());
@@ -340,8 +436,6 @@
         // Safety checks to add robustness to the codebase, so to make sure
         // that we never access invalid memory, in case that hit from the
         // posting list is corrupted.
-        ICING_ASSIGN_OR_RETURN(const float* old_vector,
-                               GetEmbeddingVector(old_hit, dimension));
         if (old_hit.basic_hit().document_id() < 0 ||
             old_hit.basic_hit().document_id() >=
                 document_id_old_to_new.size()) {
@@ -350,23 +444,36 @@
               "too small, or the index may have been corrupted.");
         }
 
-        // Construct transferred hit
+        // Construct transferred hit and add the embedding vector to the new
+        // index.
         DocumentId new_document_id =
             document_id_old_to_new[old_hit.basic_hit().document_id()];
         if (new_document_id == kInvalidDocumentId) {
           continue;
         }
+        if (new_document_id != last_new_document_id) {
+          schema_type_id =
+              document_store.GetSchemaTypeId(new_document_id, current_time_ms);
+        }
+        last_new_document_id = new_document_id;
+        if (schema_type_id == kInvalidSchemaTypeId) {
+          // This should not happen, since document store is optimized first,
+          // so that new_document_id here should be alive.
+          continue;
+        }
+        ICING_ASSIGN_OR_RETURN(
+            EmbeddingIndexingConfig::QuantizationType::Code quantization_type,
+            schema_store.GetQuantizationType(schema_type_id,
+                                             old_hit.basic_hit().section_id()));
         ICING_RETURN_IF_ERROR(new_index->MarkIndexNonEmpty());
-        uint32_t new_location = new_index->embedding_vectors_->num_elements();
+
+        ICING_ASSIGN_OR_RETURN(
+            uint32_t new_location,
+            TransferEmbeddingVector(old_hit, dimension, quantization_type,
+                                    new_index));
         new_hits.push_back(EmbeddingHit(
             BasicHit(old_hit.basic_hit().section_id(), new_document_id),
             new_location));
-
-        // 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);
       }
     }
     // No hit needs to be added to the new index.
@@ -395,8 +502,11 @@
 }
 
 libtextclassifier3::Status EmbeddingIndex::Optimize(
+    const DocumentStore* document_store, const SchemaStore* schema_store,
     const std::vector<DocumentId>& document_id_old_to_new,
     DocumentId new_last_added_document_id) {
+  ICING_RETURN_ERROR_IF_NULL(document_store);
+  ICING_RETURN_ERROR_IF_NULL(schema_store);
   if (is_empty()) {
     info().last_added_document_id = new_last_added_document_id;
     return libtextclassifier3::Status::OK;
@@ -424,9 +534,11 @@
   {
     ICING_ASSIGN_OR_RETURN(
         std::unique_ptr<EmbeddingIndex> new_index,
-        EmbeddingIndex::Create(&filesystem_, temporary_index_dir.dir()));
-    ICING_RETURN_IF_ERROR(
-        TransferIndex(document_id_old_to_new, new_index.get()));
+        EmbeddingIndex::Create(&filesystem_, temporary_index_dir.dir(), &clock_,
+                               feature_flags_));
+    ICING_RETURN_IF_ERROR(TransferIndex(*document_store, *schema_store,
+                                        document_id_old_to_new,
+                                        new_index.get()));
     new_index->set_last_added_document_id(new_last_added_document_id);
     ICING_RETURN_IF_ERROR(new_index->PersistToDisk());
   }
@@ -436,6 +548,7 @@
   flash_index_storage_.reset();
   embedding_posting_list_mapper_.reset();
   embedding_vectors_.reset();
+  quantized_embedding_vectors_.reset();
 
   if (!filesystem_.SwapFiles(temporary_index_dir.dir().c_str(),
                              working_path_.c_str())) {
@@ -448,6 +561,32 @@
   return Initialize();
 }
 
+libtextclassifier3::StatusOr<float> EmbeddingIndex::ScoreEmbeddingHit(
+    const EmbeddingScorer& scorer, const PropertyProto::VectorProto& query,
+    const EmbeddingHit& hit,
+    EmbeddingIndexingConfig::QuantizationType::Code quantization_type) const {
+  int dimension = query.values().size();
+  float semantic_score;
+  if (!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);
+  } else {
+    ICING_ASSIGN_OR_RETURN(const char* data,
+                           GetQuantizedEmbeddingVector(hit, dimension));
+    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);
+  }
+  return semantic_score;
+}
+
 libtextclassifier3::Status EmbeddingIndex::PersistMetadataToDisk() {
   return metadata_mmapped_file_->PersistToDisk();
 }
@@ -461,6 +600,7 @@
   }
   ICING_RETURN_IF_ERROR(embedding_posting_list_mapper_->PersistToDisk());
   ICING_RETURN_IF_ERROR(embedding_vectors_->PersistToDisk());
+  ICING_RETURN_IF_ERROR(quantized_embedding_vectors_->PersistToDisk());
   return libtextclassifier3::Status::OK;
 }
 
@@ -472,8 +612,11 @@
                          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());
+               embedding_vectors_crc.Get() ^
+               quantized_embedding_vectors_crc.Get());
 }
 
 libtextclassifier3::StatusOr<Crc32> EmbeddingIndex::GetStoragesChecksum()
@@ -484,8 +627,11 @@
   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());
+               embedding_vectors_crc.Get() ^
+               quantized_embedding_vectors_crc.Get());
 }
 
 }  // namespace lib
diff --git a/icing/index/embed/embedding-index.h b/icing/index/embed/embedding-index.h
index dd589c9..a2dfb7a 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/feature-flags.h"
 #include "icing/file/file-backed-vector.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/memory-mapped-file.h"
@@ -32,12 +33,18 @@
 #include "icing/file/posting_list/flash-index-storage.h"
 #include "icing/file/posting_list/posting-list-identifier.h"
 #include "icing/index/embed/embedding-hit.h"
+#include "icing/index/embed/embedding-scorer.h"
 #include "icing/index/embed/posting-list-embedding-hit-accessor.h"
 #include "icing/index/embed/posting-list-embedding-hit-serializer.h"
+#include "icing/index/embed/quantizer.h"
 #include "icing/index/hit/hit.h"
+#include "icing/schema/schema-store.h"
 #include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
 #include "icing/store/key-mapper.h"
+#include "icing/util/clock.h"
 #include "icing/util/crc32.h"
+#include "icing/util/logging.h"
 
 namespace icing {
 namespace lib {
@@ -84,7 +91,8 @@
   //   - Any error from MemoryMappedFile, FlashIndexStorage,
   //     DynamicTrieKeyMapper, or FileBackedVector.
   static libtextclassifier3::StatusOr<std::unique_ptr<EmbeddingIndex>> Create(
-      const Filesystem* filesystem, std::string working_path);
+      const Filesystem* filesystem, std::string working_path,
+      const Clock* clock, const FeatureFlags* feature_flags);
 
   static libtextclassifier3::Status Discard(const Filesystem& filesystem,
                                             const std::string& working_path) {
@@ -94,6 +102,14 @@
 
   libtextclassifier3::Status Clear();
 
+  ~EmbeddingIndex() override {
+    if (!PersistToDisk().ok()) {
+      ICING_LOG(WARNING)
+          << "Failed to persist embedding index to disk while destructing "
+          << working_path_;
+    }
+  }
+
   // Buffer an embedding pending to be added to the index. This is required
   // since EmbeddingHits added in posting lists must be decreasing, which means
   // that section ids and location indexes for a single document must be added
@@ -104,7 +120,8 @@
   //   - INVALID_ARGUMENT error if the dimension is 0.
   //   - INTERNAL_ERROR on I/O error
   libtextclassifier3::Status BufferEmbedding(
-      const BasicHit& basic_hit, const PropertyProto::VectorProto& vector);
+      const BasicHit& basic_hit, const PropertyProto::VectorProto& vector,
+      EmbeddingIndexingConfig::QuantizationType::Code quantization_type);
 
   // Commit the embedding hits in the buffer to the index.
   //
@@ -147,6 +164,7 @@
   //   - INTERNAL_ERROR on IO error, this indicates that the index may be in an
   //     invalid state and should be cleared.
   libtextclassifier3::Status Optimize(
+      const DocumentStore* document_store, const SchemaStore* schema_store,
       const std::vector<DocumentId>& document_id_old_to_new,
       DocumentId new_last_added_document_id);
 
@@ -165,6 +183,31 @@
     }
     return embedding_vectors_->array() + hit.location();
   }
+  libtextclassifier3::StatusOr<const char*> GetQuantizedEmbeddingVector(
+      const EmbeddingHit& hit, uint32_t dimension) const {
+    // 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()) {
+      return absl_ports::OutOfRangeError(
+          "Got an embedding hit that refers to a vector out of range.");
+    }
+    return quantized_embedding_vectors_->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 {
     if (is_empty()) {
@@ -180,6 +223,13 @@
     return embedding_vectors_->num_elements();
   }
 
+  int32_t GetTotalQuantizedVectorSize() const {
+    if (is_empty()) {
+      return 0;
+    }
+    return quantized_embedding_vectors_->num_elements();
+  }
+
   DocumentId last_added_document_id() const {
     return info().last_added_document_id;
   }
@@ -196,9 +246,12 @@
 
  private:
   explicit EmbeddingIndex(const Filesystem& filesystem,
-                          std::string working_path)
+                          std::string working_path, const Clock* clock,
+                          const FeatureFlags* feature_flags)
       : PersistentStorage(filesystem, std::move(working_path),
-                          kWorkingPathType) {}
+                          kWorkingPathType),
+        clock_(*clock),
+        feature_flags_(feature_flags) {}
 
   // Creates the storage data if the index is not empty. This will initialize
   // flash_index_storage_, embedding_posting_list_mapper_, embedding_vectors_.
@@ -221,6 +274,17 @@
 
   libtextclassifier3::Status Initialize();
 
+  // Transfers the embedding vector of the given hit from the current index to
+  // the new index.
+  //
+  // Returns:
+  //   - The location of the transferred vector in the new index on success.
+  //   - Any error when allocating the vector storage in the new index.
+  libtextclassifier3::StatusOr<uint32_t> TransferEmbeddingVector(
+      const EmbeddingHit& old_hit, uint32_t dimension,
+      EmbeddingIndexingConfig::QuantizationType::Code quantization_type,
+      EmbeddingIndex* new_index) const;
+
   // Transfers embedding data and hits from the current index to new_index.
   //
   // Returns:
@@ -230,6 +294,7 @@
   //     in an invalid state and the caller should handle it properly (e.g.
   //     discard and rebuild)
   libtextclassifier3::Status TransferIndex(
+      const DocumentStore& document_store, const SchemaStore& schema_store,
       const std::vector<DocumentId>& document_id_old_to_new,
       EmbeddingIndex* new_index) const;
 
@@ -251,6 +316,18 @@
 
   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.
+  //
+  // Returns:
+  //   - The location of the appended vector (i.e., the starting index within
+  //     the vector storage).
+  //   - Any error when allocating the vector storage.
+  libtextclassifier3::StatusOr<uint32_t> AppendEmbeddingVector(
+      const PropertyProto::VectorProto& vector,
+      EmbeddingIndexingConfig::QuantizationType::Code quantization_type);
+
   Crcs& crcs() override {
     return *reinterpret_cast<Crcs*>(metadata_mmapped_file_->mutable_region() +
                                     kCrcsMetadataBufferOffset);
@@ -271,6 +348,9 @@
                                           kInfoMetadataBufferOffset);
   }
 
+  const Clock& clock_;
+  const FeatureFlags* feature_flags_;  // Does not own.
+
   // In memory data:
   // Pending embedding hits with their embedding keys used for
   // embedding_posting_list_mapper_.
@@ -301,6 +381,7 @@
   //
   // null if the index is empty.
   std::unique_ptr<FileBackedVector<float>> embedding_vectors_;
+  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 6171e92..ed44120 100644
--- a/icing/index/embed/embedding-index_test.cc
+++ b/icing/index/embed/embedding-index_test.cc
@@ -28,17 +28,26 @@
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "icing/absl_ports/canonical_errors.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/index/embed/embedding-hit.h"
-#include "icing/index/embed/posting-list-embedding-hit-accessor.h"
+#include "icing/index/embed/quantizer.h"
 #include "icing/index/hit/hit.h"
+#include "icing/legacy/index/icing-filesystem.h"
 #include "icing/proto/document.pb.h"
+#include "icing/schema-builder.h"
+#include "icing/schema/schema-store.h"
+#include "icing/schema/section.h"
 #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/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
+#include "icing/util/clock.h"
 #include "icing/util/crc32.h"
-#include "icing/util/status-macros.h"
 
 namespace icing {
 namespace lib {
@@ -47,67 +56,86 @@
 
 using ::testing::ElementsAre;
 using ::testing::Eq;
+using ::testing::FloatNear;
+using ::testing::HasSubstr;
 using ::testing::IsEmpty;
+using ::testing::Pointwise;
 using ::testing::Test;
 
+static constexpr SectionId kSectionIdQuantizedEmbedding = 2;
+static constexpr float kEpsQuantized = 0.01f;
+
 class EmbeddingIndexTest : public Test {
  protected:
   void SetUp() override {
-    embedding_index_dir_ = GetTestTempDir() + "/embedding_index_test";
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+    test_dir_ = GetTestTempDir() + "/icing";
+    embedding_index_dir_ = test_dir_ + "/embedding_index";
+    document_store_dir_ = test_dir_ + "/document_store";
+    schema_store_dir_ = test_dir_ + "/schema_store";
+    filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
+    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_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));
+    document_store_ = std::move(create_result.document_store);
+
     ICING_ASSERT_OK_AND_ASSIGN(
         embedding_index_,
-        EmbeddingIndex::Create(&filesystem_, embedding_index_dir_));
+        EmbeddingIndex::Create(&filesystem_, embedding_index_dir_, &clock_,
+                               feature_flags_.get()));
+
+    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)
+                            .SetCardinality(CARDINALITY_OPTIONAL))
+                    // Quantized embedding
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("prop3")
+                            .SetDataTypeVector(EMBEDDING_INDEXING_LINEAR_SEARCH,
+                                               QUANTIZATION_TYPE_QUANTIZE_8_BIT)
+                            .SetCardinality(CARDINALITY_OPTIONAL)))
+            .Build(),
+        /*ignore_errors_and_delete_documents=*/false,
+        /*allow_circular_schema_definitions=*/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()));
   }
 
   void TearDown() override {
+    document_store_.reset();
+    schema_store_.reset();
     embedding_index_.reset();
-    filesystem_.DeleteDirectoryRecursively(embedding_index_dir_.c_str());
-  }
-
-  libtextclassifier3::StatusOr<std::vector<EmbeddingHit>> GetHits(
-      uint32_t dimension, std::string_view model_signature) {
-    return GetHits(embedding_index_.get(), dimension, model_signature);
-  }
-
-  static libtextclassifier3::StatusOr<std::vector<EmbeddingHit>> GetHits(
-      const EmbeddingIndex* embedding_index, uint32_t dimension,
-      std::string_view model_signature) {
-    std::vector<EmbeddingHit> hits;
-
-    libtextclassifier3::StatusOr<
-        std::unique_ptr<PostingListEmbeddingHitAccessor>>
-        pl_accessor_or =
-            embedding_index->GetAccessor(dimension, model_signature);
-    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())) {
-      return hits;
-    } else {
-      return std::move(pl_accessor_or).status();
-    }
-
-    while (true) {
-      ICING_ASSIGN_OR_RETURN(std::vector<EmbeddingHit> batch,
-                             pl_accessor->GetNextHitsBatch());
-      if (batch.empty()) {
-        return hits;
-      }
-      hits.insert(hits.end(), batch.begin(), batch.end());
-    }
-  }
-
-  std::vector<float> GetRawEmbeddingData() {
-    return GetRawEmbeddingData(embedding_index_.get());
-  }
-
-  static std::vector<float> GetRawEmbeddingData(
-      const EmbeddingIndex* embedding_index) {
-    ICING_ASSIGN_OR_RETURN(const float* data,
-                           embedding_index->GetRawEmbeddingData(),
-                           std::vector<float>());
-    return std::vector<float>(data,
-                              data + embedding_index->GetTotalVectorSize());
+    filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
   }
 
   libtextclassifier3::StatusOr<bool> IndexContainsMetadataOnly() {
@@ -119,8 +147,16 @@
     return sub_dirs.size() == 1 && sub_dirs[0] == "metadata";
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
+  IcingFilesystem icing_filesystem_;
+  std::string test_dir_;
   std::string embedding_index_dir_;
+  std::string schema_store_dir_;
+  std::string document_store_dir_;
+  Clock clock_;
+  std::unique_ptr<SchemaStore> schema_store_;
+  std::unique_ptr<DocumentStore> document_store_;
   std::unique_ptr<EmbeddingIndex> embedding_index_;
 };
 
@@ -128,6 +164,19 @@
   EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(true));
 }
 
+TEST_F(EmbeddingIndexTest, InitializationShouldFailWithNullPointer) {
+  std::string embedding_index_dir =
+      GetTestTempDir() + "/embedding_index_test_local";
+
+  EXPECT_THAT(EmbeddingIndex::Create(nullptr, embedding_index_dir, &clock_,
+                                     feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+
+  EXPECT_THAT(EmbeddingIndex::Create(&filesystem_, embedding_index_dir, nullptr,
+                                     feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+}
+
 TEST_F(EmbeddingIndexTest,
        InitializationShouldFailWithoutPersistToDiskOrDestruction) {
   // 1. Create index and confirm that data was properly added.
@@ -135,20 +184,22 @@
       GetTestTempDir() + "/embedding_index_test_local";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<EmbeddingIndex> embedding_index,
-      EmbeddingIndex::Create(&filesystem_, embedding_index_dir));
+      EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
+                             feature_flags_.get()));
 
   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));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index->CommitBufferToIndex());
   embedding_index->set_last_added_document_id(0);
 
   EXPECT_THAT(
-      GetHits(embedding_index.get(), /*dimension=*/3,
-              /*model_signature=*/"model"),
+      GetEmbeddingHitsFromIndex(embedding_index.get(), /*dimension=*/3,
+                                /*model_signature=*/"model"),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/0), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingData(embedding_index.get()),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index.get()),
               ElementsAre(0.1, 0.2, 0.3));
   EXPECT_EQ(embedding_index->last_added_document_id(), 0);
   // GetChecksum should succeed without updating the checksum.
@@ -156,7 +207,8 @@
 
   // 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),
+  EXPECT_THAT(EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
+                                     feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 
   embedding_index.reset();
@@ -169,20 +221,22 @@
       GetTestTempDir() + "/embedding_index_test_local";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<EmbeddingIndex> embedding_index,
-      EmbeddingIndex::Create(&filesystem_, embedding_index_dir));
+      EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
+                             feature_flags_.get()));
 
   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));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index->CommitBufferToIndex());
   embedding_index->set_last_added_document_id(0);
 
   EXPECT_THAT(
-      GetHits(embedding_index.get(), /*dimension=*/3,
-              /*model_signature=*/"model"),
+      GetEmbeddingHitsFromIndex(embedding_index.get(), /*dimension=*/3,
+                                /*model_signature=*/"model"),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/0), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingData(embedding_index.get()),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index.get()),
               ElementsAre(0.1, 0.2, 0.3));
   EXPECT_EQ(embedding_index->last_added_document_id(), 0);
 
@@ -194,14 +248,15 @@
   // 3. Create another index and confirm that the data is still there.
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<EmbeddingIndex> embedding_index_two,
-      EmbeddingIndex::Create(&filesystem_, embedding_index_dir));
+      EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
+                             feature_flags_.get()));
 
   EXPECT_THAT(
-      GetHits(embedding_index_two.get(), /*dimension=*/3,
-              /*model_signature=*/"model"),
+      GetEmbeddingHitsFromIndex(embedding_index_two.get(), /*dimension=*/3,
+                                /*model_signature=*/"model"),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/0), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingData(embedding_index_two.get()),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_two.get()),
               ElementsAre(0.1, 0.2, 0.3));
   EXPECT_EQ(embedding_index_two->last_added_document_id(), 0);
 
@@ -216,20 +271,22 @@
       GetTestTempDir() + "/embedding_index_test_local";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<EmbeddingIndex> embedding_index,
-      EmbeddingIndex::Create(&filesystem_, embedding_index_dir));
+      EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
+                             feature_flags_.get()));
 
   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));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index->CommitBufferToIndex());
   embedding_index->set_last_added_document_id(0);
 
   EXPECT_THAT(
-      GetHits(embedding_index.get(), /*dimension=*/3,
-              /*model_signature=*/"model"),
+      GetEmbeddingHitsFromIndex(embedding_index.get(), /*dimension=*/3,
+                                /*model_signature=*/"model"),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/0), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingData(embedding_index.get()),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index.get()),
               ElementsAre(0.1, 0.2, 0.3));
   EXPECT_EQ(embedding_index->last_added_document_id(), 0);
 
@@ -239,14 +296,15 @@
   // 3. Create another index and confirm that the data is still there.
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<EmbeddingIndex> embedding_index_two,
-      EmbeddingIndex::Create(&filesystem_, embedding_index_dir));
+      EmbeddingIndex::Create(&filesystem_, embedding_index_dir, &clock_,
+                             feature_flags_.get()));
 
   EXPECT_THAT(
-      GetHits(embedding_index_two.get(), /*dimension=*/3,
-              /*model_signature=*/"model"),
+      GetEmbeddingHitsFromIndex(embedding_index_two.get(), /*dimension=*/3,
+                                /*model_signature=*/"model"),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/0), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingData(embedding_index_two.get()),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_two.get()),
               ElementsAre(0.1, 0.2, 0.3));
   EXPECT_EQ(embedding_index_two->last_added_document_id(), 0);
 
@@ -255,18 +313,78 @@
   filesystem_.DeleteDirectoryRecursively(embedding_index_dir.c_str());
 }
 
+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_->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));
+}
+
+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));
+  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),
+              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));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(0);
 
   EXPECT_THAT(
-      GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+      GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                /*model_signature=*/"model"),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/0), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingData(), ElementsAre(0.1, 0.2, 0.3));
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+              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));
+  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"),
+              IsOkAndHolds(ElementsAre(hit)));
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
+              Eq(3 + sizeof(Quantizer)));
+  EXPECT_THAT(
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
+                                                     hit,
+                                                     /*dimension=*/3),
+      IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.1, 0.2, 0.3})));
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
@@ -275,41 +393,81 @@
   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));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector1,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector2));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector2,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(0);
 
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/0),
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/3))));
-  EXPECT_THAT(GetRawEmbeddingData(),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
               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));
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/0), vector2,
+      QUANTIZATION_TYPE_QUANTIZE_8_BIT));
+  ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
+  embedding_index_->set_last_added_document_id(0);
+
+  EmbeddingHit hit1(BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/0),
+                    /*location=*/0);
+  EmbeddingHit hit2(BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/0),
+                    /*location=*/3 + sizeof(Quantizer));
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
+              IsOkAndHolds(ElementsAre(hit1, hit2)));
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
+              Eq(2 * (3 + sizeof(Quantizer))));  // Two quantized vectors
+  EXPECT_THAT(
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
+                                                     hit1, /*dimension=*/3),
+      IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.1, 0.2, 0.3})));
+  EXPECT_THAT(
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
+                                                     hit2, /*dimension=*/3),
+      IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {-0.1, -0.2, -0.3})));
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), 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));
+      BasicHit(/*section_id=*/5, /*document_id=*/0), vector1,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/2, /*document_id=*/0), vector2));
+      BasicHit(/*section_id=*/2, /*document_id=*/0), vector2,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(0);
 
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/2, /*document_id=*/0),
                                /*location=*/3),
                   EmbeddingHit(BasicHit(/*section_id=*/5, /*document_id=*/0),
                                /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingData(),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
               ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
@@ -319,19 +477,22 @@
   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));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector1,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/1), vector2));
+      BasicHit(/*section_id=*/0, /*document_id=*/1), vector2,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(1);
 
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/1),
                                /*location=*/3),
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingData(),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
               ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
 }
@@ -341,24 +502,30 @@
   PropertyProto::VectorProto vector2 =
       CreateVector("model2", {-0.1, -0.2, -0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector1));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector1,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector2));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector2,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(0);
 
-  EXPECT_THAT(GetHits(/*dimension=*/2, /*model_signature=*/"model1"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/2,
+                                        /*model_signature=*/"model1"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/0))));
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model2"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model2"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/2))));
-  EXPECT_THAT(
-      GetHits(/*dimension=*/5, /*model_signature=*/"non-existent-model"),
-      IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(GetRawEmbeddingData(), ElementsAre(0.1, 0.2, -0.1, -0.2, -0.3));
+  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));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
@@ -368,21 +535,26 @@
   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));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector1,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector2));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector2,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(0);
 
-  EXPECT_THAT(GetHits(/*dimension=*/2, /*model_signature=*/"model"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/2,
+                                        /*model_signature=*/"model"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/0))));
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/2))));
-  EXPECT_THAT(GetRawEmbeddingData(), ElementsAre(0.1, 0.2, -0.1, -0.2, -0.3));
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+              ElementsAre(0.1, 0.2, -0.1, -0.2, -0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
@@ -393,22 +565,41 @@
     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));
-    ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-        BasicHit(/*section_id=*/2, /*document_id=*/1), vector2));
-    ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
-    embedding_index_->set_last_added_document_id(1);
+    PropertyProto::VectorProto vector3 = CreateVector("model", {0.4, 0.5, 0.6});
 
-    EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
-                IsOkAndHolds(ElementsAre(
-                    EmbeddingHit(BasicHit(/*section_id=*/2, /*document_id=*/1),
-                                 /*location=*/3),
-                    EmbeddingHit(BasicHit(/*section_id=*/1, /*document_id=*/0),
-                                 /*location=*/0))));
-    EXPECT_THAT(GetRawEmbeddingData(),
+    ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+        BasicHit(/*section_id=*/1, /*document_id=*/0), vector1,
+        QUANTIZATION_TYPE_NONE));
+    ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+        BasicHit(/*section_id=*/2, /*document_id=*/1), vector2,
+        QUANTIZATION_TYPE_NONE));
+    ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+        BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2), vector3,
+        QUANTIZATION_TYPE_QUANTIZE_8_BIT));
+    ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
+    embedding_index_->set_last_added_document_id(2);
+
+    EmbeddingHit hit1(BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2),
+                      /*location=*/0);
+    EmbeddingHit hit2(BasicHit(/*section_id=*/2, /*document_id=*/1),
+                      /*location=*/3);
+    EmbeddingHit hit3(BasicHit(/*section_id=*/1, /*document_id=*/0),
+                      /*location=*/0);
+
+    EXPECT_THAT(
+        GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                  /*model_signature=*/"model"),
+        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_EQ(embedding_index_->last_added_document_id(), 1);
+    EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
+                Eq(3 + sizeof(Quantizer)));
+    EXPECT_THAT(
+        GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
+                                                       hit1,
+                                                       /*dimension=*/3),
+        IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), vector3.values())));
+    EXPECT_EQ(embedding_index_->last_added_document_id(), 2);
     EXPECT_FALSE(embedding_index_->is_empty());
     EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(false));
 
@@ -416,7 +607,9 @@
     ICING_ASSERT_OK(embedding_index_->Clear());
     EXPECT_TRUE(embedding_index_->is_empty());
     EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(true));
-    EXPECT_THAT(GetRawEmbeddingData(), IsEmpty());
+    EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+                IsEmpty());
+    EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(), Eq(0));
     EXPECT_EQ(embedding_index_->last_added_document_id(), kInvalidDocumentId);
   }
 }
@@ -428,22 +621,40 @@
     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));
-    ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-        BasicHit(/*section_id=*/2, /*document_id=*/1), vector2));
-    ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
-    embedding_index_->set_last_added_document_id(1);
+    PropertyProto::VectorProto vector3 = CreateVector("model", {0.4, 0.5, 0.6});
 
-    EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
-                IsOkAndHolds(ElementsAre(
-                    EmbeddingHit(BasicHit(/*section_id=*/2, /*document_id=*/1),
-                                 /*location=*/3),
-                    EmbeddingHit(BasicHit(/*section_id=*/1, /*document_id=*/0),
-                                 /*location=*/0))));
-    EXPECT_THAT(GetRawEmbeddingData(),
+    ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+        BasicHit(/*section_id=*/1, /*document_id=*/0), vector1,
+        QUANTIZATION_TYPE_NONE));
+    ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+        BasicHit(/*section_id=*/2, /*document_id=*/1), vector2,
+        QUANTIZATION_TYPE_NONE));
+    ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+        BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2), vector3,
+        QUANTIZATION_TYPE_QUANTIZE_8_BIT));
+    ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
+    embedding_index_->set_last_added_document_id(2);
+
+    EmbeddingHit hit1(BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2),
+                      /*location=*/0);
+    EmbeddingHit hit2(BasicHit(/*section_id=*/2, /*document_id=*/1),
+                      /*location=*/3);
+    EmbeddingHit hit3(BasicHit(/*section_id=*/1, /*document_id=*/0),
+                      /*location=*/0);
+    EXPECT_THAT(
+        GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                  /*model_signature=*/"model"),
+        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_EQ(embedding_index_->last_added_document_id(), 1);
+    EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
+                Eq(3 + sizeof(Quantizer)));
+    EXPECT_THAT(
+        GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
+                                                       hit1,
+                                                       /*dimension=*/3),
+        IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), vector3.values())));
+    EXPECT_EQ(embedding_index_->last_added_document_id(), 2);
     EXPECT_FALSE(embedding_index_->is_empty());
     EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(false));
 
@@ -452,10 +663,13 @@
     EmbeddingIndex::Discard(filesystem_, embedding_index_dir_);
     ICING_ASSERT_OK_AND_ASSIGN(
         embedding_index_,
-        EmbeddingIndex::Create(&filesystem_, embedding_index_dir_));
+        EmbeddingIndex::Create(&filesystem_, embedding_index_dir_, &clock_,
+                               feature_flags_.get()));
     EXPECT_TRUE(embedding_index_->is_empty());
     EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(true));
-    EXPECT_THAT(GetRawEmbeddingData(), IsEmpty());
+    EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+                IsEmpty());
+    EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(), Eq(0));
     EXPECT_EQ(embedding_index_->last_added_document_id(), kInvalidDocumentId);
   }
 }
@@ -464,7 +678,8 @@
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   EXPECT_TRUE(embedding_index_->is_empty());
   EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(true));
-  EXPECT_THAT(GetRawEmbeddingData(), IsEmpty());
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(), Eq(0));
 }
 
 TEST_F(EmbeddingIndexTest, MultipleCommits) {
@@ -473,20 +688,23 @@
       CreateVector("model", {-0.1, -0.2, -0.3});
 
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/1, /*document_id=*/0), vector1));
+      BasicHit(/*section_id=*/1, /*document_id=*/0), vector1,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector2));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector2,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/3),
                   EmbeddingHit(BasicHit(/*section_id=*/1, /*document_id=*/0),
                                /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingData(),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
               ElementsAre(0.1, 0.2, 0.3, -0.1, -0.2, -0.3));
 }
 
@@ -497,11 +715,13 @@
       CreateVector("model", {-0.1, -0.2, -0.3});
 
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector1));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector1,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/1, /*document_id=*/0), vector2));
+      BasicHit(/*section_id=*/1, /*document_id=*/0), vector2,
+      QUANTIZATION_TYPE_NONE));
   // Posting list with delta encoding can only allow decreasing values.
   EXPECT_THAT(embedding_index_->CommitBufferToIndex(),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
@@ -513,11 +733,13 @@
       CreateVector("model", {-0.1, -0.2, -0.3});
 
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/1), vector1));
+      BasicHit(/*section_id=*/0, /*document_id=*/1), vector1,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector2));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector2,
+      QUANTIZATION_TYPE_NONE));
   // 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.
@@ -525,63 +747,184 @@
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
+TEST_F(EmbeddingIndexTest, OptimizeShouldFailWithNullPointer) {
+  EXPECT_THAT(embedding_index_->Optimize(
+                  /*document_store=*/nullptr, schema_store_.get(),
+                  /*document_id_old_to_new=*/{},
+                  /*new_last_added_document_id=*/kInvalidDocumentId),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+
+  EXPECT_THAT(embedding_index_->Optimize(
+                  document_store_.get(), /*schema_store=*/nullptr,
+                  /*document_id_old_to_new=*/{},
+                  /*new_last_added_document_id=*/kInvalidDocumentId),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+}
+
+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));
+  ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
+  embedding_index_->set_last_added_document_id(2);
+
+  // Optimize should fail because the provided document_id_old_to_new map does
+  // not contain an entry for document id 2.
+  EXPECT_THAT(embedding_index_
+                  ->Optimize(document_store_.get(), schema_store_.get(),
+                             /*document_id_old_to_new=*/{0, 1},
+                             /*new_last_added_document_id=*/2)
+                  .error_message(),
+              HasSubstr("The provided map is too small"));
+}
+
 TEST_F(EmbeddingIndexTest, EmptyOptimizeIsOk) {
   ICING_ASSERT_OK(embedding_index_->Optimize(
+      document_store_.get(), schema_store_.get(),
       /*document_id_old_to_new=*/{},
       /*new_last_added_document_id=*/kInvalidDocumentId));
   EXPECT_TRUE(embedding_index_->is_empty());
   EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(true));
-  EXPECT_THAT(GetRawEmbeddingData(), IsEmpty());
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(), 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));
+      BasicHit(/*section_id=*/0, /*document_id=*/2), vector,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(2);
 
   // Before optimize
   EXPECT_THAT(
-      GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+      GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                /*model_signature=*/"model"),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/2), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingData(), ElementsAre(0.1, 0.2, 0.3));
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+              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
   // not changed.
-  ICING_ASSERT_OK(embedding_index_->Optimize(
-      /*document_id_old_to_new=*/{0, 1, 2},
-      /*new_last_added_document_id=*/2));
+  ICING_ASSERT_OK(
+      embedding_index_->Optimize(document_store_.get(), schema_store_.get(),
+                                 /*document_id_old_to_new=*/{0, 1, 2},
+                                 /*new_last_added_document_id=*/2));
   EXPECT_THAT(
-      GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+      GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                /*model_signature=*/"model"),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/2), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingData(), ElementsAre(0.1, 0.2, 0.3));
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+              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
   // updated correctly.
   ICING_ASSERT_OK(embedding_index_->Optimize(
+      document_store_.get(), schema_store_.get(),
       /*document_id_old_to_new=*/{0, kInvalidDocumentId, 1},
       /*new_last_added_document_id=*/1));
   EXPECT_THAT(
-      GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+      GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                /*model_signature=*/"model"),
       IsOkAndHolds(ElementsAre(EmbeddingHit(
           BasicHit(/*section_id=*/0, /*document_id=*/1), /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingData(), ElementsAre(0.1, 0.2, 0.3));
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+              ElementsAre(0.1, 0.2, 0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
 
   // Run optimize to delete the document.
   ICING_ASSERT_OK(embedding_index_->Optimize(
+      document_store_.get(), schema_store_.get(),
       /*document_id_old_to_new=*/{0, kInvalidDocumentId},
       /*new_last_added_document_id=*/0));
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
               IsOkAndHolds(IsEmpty()));
   EXPECT_TRUE(embedding_index_->is_empty());
   EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(true));
-  EXPECT_THAT(GetRawEmbeddingData(), IsEmpty());
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), 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));
+  ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
+  embedding_index_->set_last_added_document_id(2);
+
+  // Before optimize
+  EmbeddingHit hit(BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2),
+                   /*location=*/0);
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
+              IsOkAndHolds(ElementsAre(hit)));
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
+              Eq(3 + sizeof(Quantizer)));
+  EXPECT_THAT(
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
+                                                     hit, /*dimension=*/3),
+      IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.1, 0.2, 0.3})));
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
+  EXPECT_EQ(embedding_index_->last_added_document_id(), 2);
+
+  // Run optimize without deleting any documents, and check that the index is
+  // not changed
+  ICING_ASSERT_OK(
+      embedding_index_->Optimize(document_store_.get(), schema_store_.get(),
+                                 /*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"),
+              IsOkAndHolds(ElementsAre(hit)));
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
+              Eq(3 + sizeof(Quantizer)));
+  EXPECT_THAT(
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
+                                                     hit, /*dimension=*/3),
+      IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.1, 0.2, 0.3})));
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), 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
+  // updated correctly
+  ICING_ASSERT_OK(embedding_index_->Optimize(
+      document_store_.get(), schema_store_.get(),
+      /*document_id_old_to_new=*/{0, kInvalidDocumentId, 1},
+      /*new_last_added_document_id=*/1));
+  hit = EmbeddingHit(BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/1),
+                     /*location=*/0);
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
+              IsOkAndHolds(ElementsAre(hit)));
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(),
+              Eq(3 + sizeof(Quantizer)));
+  EXPECT_THAT(
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
+                                                     hit, /*dimension=*/3),
+      IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.1, 0.2, 0.3})));
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
+  EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
+
+  // Run optimize to delete the document
+  ICING_ASSERT_OK(embedding_index_->Optimize(
+      document_store_.get(), schema_store_.get(),
+      /*document_id_old_to_new=*/{0, kInvalidDocumentId},
+      /*new_last_added_document_id=*/0));
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
+              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_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
@@ -589,63 +932,108 @@
   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));
+      BasicHit(/*section_id=*/0, /*document_id=*/2), vector1,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/2), vector2));
+      BasicHit(/*section_id=*/0, /*document_id=*/2), vector2,
+      QUANTIZATION_TYPE_NONE));
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2), vector3,
+      QUANTIZATION_TYPE_QUANTIZE_8_BIT));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(2);
 
   // Before optimize
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+  EmbeddingHit quantized_hit(
+      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/2),
+      /*location=*/0);
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/2),
                                /*location=*/0),
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/2),
-                               /*location=*/3))));
-  EXPECT_THAT(GetRawEmbeddingData(),
+                               /*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())));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 2);
 
   // Run optimize without deleting any documents, and check that the index is
   // not changed.
-  ICING_ASSERT_OK(embedding_index_->Optimize(
-      /*document_id_old_to_new=*/{0, 1, 2},
-      /*new_last_added_document_id=*/2));
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+  ICING_ASSERT_OK(
+      embedding_index_->Optimize(document_store_.get(), schema_store_.get(),
+                                 /*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"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/2),
                                /*location=*/0),
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/2),
-                               /*location=*/3))));
-  EXPECT_THAT(GetRawEmbeddingData(),
+                               /*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())));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 2);
 
   // Run optimize to map document id 2 to 1, and check that the index is
   // updated correctly.
   ICING_ASSERT_OK(embedding_index_->Optimize(
+      document_store_.get(), schema_store_.get(),
       /*document_id_old_to_new=*/{0, kInvalidDocumentId, 1},
       /*new_last_added_document_id=*/1));
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+  quantized_hit =
+      EmbeddingHit(BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/1),
+                   /*location=*/0);
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/1),
                                /*location=*/0),
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/1),
-                               /*location=*/3))));
-  EXPECT_THAT(GetRawEmbeddingData(),
+                               /*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())));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
 
   // Run optimize to delete the document.
   ICING_ASSERT_OK(embedding_index_->Optimize(
+      document_store_.get(), schema_store_.get(),
       /*document_id_old_to_new=*/{0, kInvalidDocumentId},
       /*new_last_added_document_id=*/0));
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
               IsOkAndHolds(IsEmpty()));
   EXPECT_TRUE(embedding_index_->is_empty());
   EXPECT_THAT(IndexContainsMetadataOnly(), IsOkAndHolds(true));
-  EXPECT_THAT(GetRawEmbeddingData(), IsEmpty());
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
+  EXPECT_THAT(embedding_index_->GetTotalQuantizedVectorSize(), Eq(0));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
@@ -654,26 +1042,46 @@
   PropertyProto::VectorProto vector2 = CreateVector("model", {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});
+
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector1));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector1,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/1, /*document_id=*/0), vector2));
+      BasicHit(/*section_id=*/1, /*document_id=*/0), vector2,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/1), vector3));
+      BasicHit(/*section_id=*/0, /*document_id=*/1), vector3,
+      QUANTIZATION_TYPE_NONE));
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/1), vector4,
+      QUANTIZATION_TYPE_QUANTIZE_8_BIT));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(1);
 
   // Before optimize
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+  EmbeddingHit quantized_hit(
+      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/1),
+      /*location=*/0);
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/1),
                                /*location=*/6),
+                  quantized_hit,
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/0),
                   EmbeddingHit(BasicHit(/*section_id=*/1, /*document_id=*/0),
                                /*location=*/3))));
-  EXPECT_THAT(GetRawEmbeddingData(),
+  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(),
+              Eq(3 + sizeof(Quantizer)));
+  EXPECT_THAT(
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
+                                                     quantized_hit,
+                                                     /*dimension=*/3),
+      IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), vector4.values())));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
 
   // Run optimize without deleting any documents. It is expected to see that the
@@ -683,32 +1091,57 @@
   // Also keep in mind that once the raw data is rearranged, calling another
   // Optimize subsequently will not change the raw data again.
   for (int i = 0; i < 2; i++) {
-    ICING_ASSERT_OK(embedding_index_->Optimize(
-        /*document_id_old_to_new=*/{0, 1},
-        /*new_last_added_document_id=*/1));
-    EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
-                IsOkAndHolds(ElementsAre(
-                    EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/1),
-                                 /*location=*/0),
-                    EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
-                                 /*location=*/3),
-                    EmbeddingHit(BasicHit(/*section_id=*/1, /*document_id=*/0),
-                                 /*location=*/6))));
-    EXPECT_THAT(GetRawEmbeddingData(),
+    ICING_ASSERT_OK(
+        embedding_index_->Optimize(document_store_.get(), schema_store_.get(),
+                                   /*document_id_old_to_new=*/{0, 1},
+                                   /*new_last_added_document_id=*/1));
+    EXPECT_THAT(
+        GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                  /*model_signature=*/"model"),
+        IsOkAndHolds(ElementsAre(
+            EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/1),
+                         /*location=*/0),
+            quantized_hit,
+            EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
+                         /*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),
+        IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), vector4.values())));
     EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
   }
 
   // Run optimize to delete document 0, and check that the index is
   // updated correctly.
   ICING_ASSERT_OK(embedding_index_->Optimize(
+      document_store_.get(), schema_store_.get(),
       /*document_id_old_to_new=*/{kInvalidDocumentId, 0},
       /*new_last_added_document_id=*/0));
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+  quantized_hit =
+      EmbeddingHit(BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/0),
+                   /*location=*/0);
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
-                               /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingData(), ElementsAre(-0.1, -0.2, -0.3));
+                               /*location=*/0),
+                  quantized_hit)));
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+              ElementsAre(-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), vector4.values())));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
@@ -718,26 +1151,31 @@
   PropertyProto::VectorProto vector3 =
       CreateVector("model2", {-0.1, -0.2, -0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector1));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector1,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/1), vector2));
+      BasicHit(/*section_id=*/0, /*document_id=*/1), vector2,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/1, /*document_id=*/1), vector3));
+      BasicHit(/*section_id=*/1, /*document_id=*/1), vector3,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(1);
 
   // Before optimize
-  EXPECT_THAT(GetHits(/*dimension=*/2, /*model_signature=*/"model1"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/2,
+                                        /*model_signature=*/"model1"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/1),
                                /*location=*/2),
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/0))));
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model2"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model2"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/1, /*document_id=*/1),
                                /*location=*/4))));
-  EXPECT_THAT(GetRawEmbeddingData(),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
               ElementsAre(0.1, 0.2, 1, 2, -0.1, -0.2, -0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
 
@@ -750,20 +1188,25 @@
   // Also keep in mind that once the raw data is rearranged, calling another
   // Optimize subsequently will not change the raw data again.
   for (int i = 0; i < 2; i++) {
-    ICING_ASSERT_OK(embedding_index_->Optimize(
-        /*document_id_old_to_new=*/{0, 1},
-        /*new_last_added_document_id=*/1));
-    EXPECT_THAT(GetHits(/*dimension=*/2, /*model_signature=*/"model1"),
-                IsOkAndHolds(ElementsAre(
-                    EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/1),
-                                 /*location=*/0),
-                    EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
-                                 /*location=*/2))));
-    EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model2"),
-                IsOkAndHolds(ElementsAre(
-                    EmbeddingHit(BasicHit(/*section_id=*/1, /*document_id=*/1),
-                                 /*location=*/4))));
-    EXPECT_THAT(GetRawEmbeddingData(),
+    ICING_ASSERT_OK(
+        embedding_index_->Optimize(document_store_.get(), schema_store_.get(),
+                                   /*document_id_old_to_new=*/{0, 1},
+                                   /*new_last_added_document_id=*/1));
+    EXPECT_THAT(
+        GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/2,
+                                  /*model_signature=*/"model1"),
+        IsOkAndHolds(ElementsAre(
+            EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/1),
+                         /*location=*/0),
+            EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
+                         /*location=*/2))));
+    EXPECT_THAT(
+        GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                  /*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));
     EXPECT_EQ(embedding_index_->last_added_document_id(), 1);
   }
@@ -771,15 +1214,19 @@
   // Run optimize to delete document 1, and check that the index is
   // updated correctly.
   ICING_ASSERT_OK(embedding_index_->Optimize(
+      document_store_.get(), schema_store_.get(),
       /*document_id_old_to_new=*/{0, kInvalidDocumentId},
       /*new_last_added_document_id=*/0));
-  EXPECT_THAT(GetHits(/*dimension=*/2, /*model_signature=*/"model1"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/2,
+                                        /*model_signature=*/"model1"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/0))));
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model2"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model2"),
               IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(GetRawEmbeddingData(), ElementsAre(0.1, 0.2));
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+              ElementsAre(0.1, 0.2));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
@@ -789,36 +1236,45 @@
   PropertyProto::VectorProto vector2 =
       CreateVector("model2", {-0.1, -0.2, -0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/0, /*document_id=*/0), vector1));
+      BasicHit(/*section_id=*/0, /*document_id=*/0), vector1,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(/*section_id=*/1, /*document_id=*/1), vector2));
+      BasicHit(/*section_id=*/1, /*document_id=*/1), vector2,
+      QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
   embedding_index_->set_last_added_document_id(1);
 
   // Before optimize
-  EXPECT_THAT(GetHits(/*dimension=*/2, /*model_signature=*/"model1"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/2,
+                                        /*model_signature=*/"model1"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/0, /*document_id=*/0),
                                /*location=*/0))));
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model2"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model2"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/1, /*document_id=*/1),
                                /*location=*/2))));
-  EXPECT_THAT(GetRawEmbeddingData(), ElementsAre(0.1, 0.2, -0.1, -0.2, -0.3));
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+              ElementsAre(0.1, 0.2, -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
   // updated correctly.
   ICING_ASSERT_OK(embedding_index_->Optimize(
+      document_store_.get(), schema_store_.get(),
       /*document_id_old_to_new=*/{kInvalidDocumentId, 0},
       /*new_last_added_document_id=*/0));
-  EXPECT_THAT(GetHits(/*dimension=*/2, /*model_signature=*/"model1"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/2,
+                                        /*model_signature=*/"model1"),
               IsOkAndHolds(IsEmpty()));
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model2"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model2"),
               IsOkAndHolds(ElementsAre(
                   EmbeddingHit(BasicHit(/*section_id=*/1, /*document_id=*/0),
                                /*location=*/0))));
-  EXPECT_THAT(GetRawEmbeddingData(), ElementsAre(-0.1, -0.2, -0.3));
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+              ElementsAre(-0.1, -0.2, -0.3));
   EXPECT_EQ(embedding_index_->last_added_document_id(), 0);
 }
 
diff --git a/icing/index/embed/embedding-scorer.cc b/icing/index/embed/embedding-scorer.cc
index 0d84e01..0a3ed6c 100644
--- a/icing/index/embed/embedding-scorer.cc
+++ b/icing/index/embed/embedding-scorer.cc
@@ -15,12 +15,15 @@
 #include "icing/index/embed/embedding-scorer.h"
 
 #include <cmath>
+#include <cstdint>
 #include <memory>
 #include <string>
+#include <type_traits>
 
 #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/quantizer.h"
 #include "icing/proto/search.pb.h"
 
 namespace icing {
@@ -28,31 +31,55 @@
 
 namespace {
 
-float CalculateDotProduct(int dimension, const float* v1, const float* v2) {
+template <typename T>
+inline std::enable_if_t<std::is_same<T, float>::value, float> ToFloat(T value) {
+  return value;
+}
+
+template <typename T>
+inline std::enable_if_t<std::is_same<T, float>::value, float> ToFloat(
+    T value, const Quantizer&) {
+  return value;
+}
+
+template <typename T>
+inline std::enable_if_t<std::is_same<T, uint8_t>::value, float> ToFloat(
+    T quantized, const Quantizer& quantizer) {
+  return quantizer.Dequantize(quantized);
+}
+
+template <typename T1, typename T2, typename... Args>
+float CalculateDotProduct(int dimension, const T1* v1, const T2* v2,
+                          const Args&... args) {
   float dot_product = 0.0;
   for (int i = 0; i < dimension; ++i) {
-    dot_product += v1[i] * v2[i];
+    dot_product += ToFloat(v1[i], args...) * ToFloat(v2[i], args...);
   }
   return dot_product;
 }
 
-float CalculateNorm2(int dimension, const float* v) {
-  return std::sqrt(CalculateDotProduct(dimension, v, v));
+template <typename T, typename... Args>
+float CalculateNorm2(int dimension, const T* v, const Args&... args) {
+  return std::sqrt(CalculateDotProduct(dimension, v, v, args...));
 }
 
-float CalculateCosine(int dimension, const float* v1, const float* v2) {
-  float divisor = CalculateNorm2(dimension, v1) * CalculateNorm2(dimension, v2);
+template <typename T1, typename T2, typename... Args>
+float CalculateCosine(int dimension, const T1* v1, const T2* v2,
+                      const Args&... args) {
+  float divisor = CalculateNorm2(dimension, v1, args...) *
+                  CalculateNorm2(dimension, v2, args...);
   if (divisor == 0.0) {
     return 0.0;
   }
-  return CalculateDotProduct(dimension, v1, v2) / divisor;
+  return CalculateDotProduct(dimension, v1, v2, args...) / divisor;
 }
 
-float CalculateEuclideanDistance(int dimension, const float* v1,
-                                 const float* v2) {
+template <typename T1, typename T2, typename... Args>
+float CalculateEuclideanDistance(int dimension, const T1* v1, const T2* v2,
+                                 const Args&... args) {
   float result = 0.0;
   for (int i = 0; i < dimension; ++i) {
-    float diff = v1[i] - v2[i];
+    float diff = ToFloat(v1[i], args...) - ToFloat(v2[i], args...);
     result += diff * diff;
   }
   return std::sqrt(result);
@@ -91,5 +118,23 @@
   return CalculateEuclideanDistance(dimension, v1, v2);
 }
 
+float CosineEmbeddingScorer::Score(int dimension, const float* v1,
+                                   const uint8_t* v2,
+                                   const Quantizer& quantizer) const {
+  return CalculateCosine(dimension, v1, v2, quantizer);
+}
+
+float DotProductEmbeddingScorer::Score(int dimension, const float* v1,
+                                       const uint8_t* v2,
+                                       const Quantizer& quantizer) const {
+  return CalculateDotProduct(dimension, v1, v2, quantizer);
+}
+
+float EuclideanDistanceEmbeddingScorer::Score(
+    int dimension, const float* v1, const uint8_t* v2,
+    const Quantizer& quantizer) const {
+  return CalculateEuclideanDistance(dimension, v1, v2, quantizer);
+}
+
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/index/embed/embedding-scorer.h b/icing/index/embed/embedding-scorer.h
index 8caf0bc..b0e0f75 100644
--- a/icing/index/embed/embedding-scorer.h
+++ b/icing/index/embed/embedding-scorer.h
@@ -15,9 +15,11 @@
 #ifndef ICING_INDEX_EMBED_EMBEDDING_SCORER_H_
 #define ICING_INDEX_EMBED_EMBEDDING_SCORER_H_
 
+#include <cstdint>
 #include <memory>
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/index/embed/quantizer.h"
 #include "icing/proto/search.pb.h"
 
 namespace icing {
@@ -27,8 +29,11 @@
  public:
   static libtextclassifier3::StatusOr<std::unique_ptr<EmbeddingScorer>> Create(
       SearchSpecProto::EmbeddingQueryMetricType::Code metric_type);
+
   virtual float Score(int dimension, const float* v1,
                       const float* v2) const = 0;
+  virtual float Score(int dimension, const float* v1, const uint8_t* v2,
+                      const Quantizer& quantizer) const = 0;
 
   virtual ~EmbeddingScorer() = default;
 };
@@ -36,16 +41,22 @@
 class CosineEmbeddingScorer : public EmbeddingScorer {
  public:
   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;
 };
 
 class DotProductEmbeddingScorer : public EmbeddingScorer {
  public:
   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;
 };
 
 class EuclideanDistanceEmbeddingScorer : public EmbeddingScorer {
  public:
   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;
 };
 
 }  // namespace lib
diff --git a/icing/index/embed/embedding-scorer_test.cc b/icing/index/embed/embedding-scorer_test.cc
new file mode 100644
index 0000000..162b969
--- /dev/null
+++ b/icing/index/embed/embedding-scorer_test.cc
@@ -0,0 +1,126 @@
+// Copyright (C) 2024 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/embedding-scorer.h"
+
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "icing/index/embed/quantizer.h"
+#include "icing/testing/common-matchers.h"
+
+namespace icing {
+namespace lib {
+
+namespace {
+
+std::vector<uint8_t> QuantizeVector(std::vector<float> v,
+                                    const Quantizer& quantizer) {
+  std::vector<uint8_t> quantized;
+  quantized.reserve(v.size());
+  for (float value : v) {
+    quantized.push_back(quantizer.Quantize(value));
+  }
+  return quantized;
+}
+
+TEST(EmbeddingScorerTest, DotProduct) {
+  constexpr float eps_quantized = 0.01f;
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<EmbeddingScorer> embedding_scorer,
+      EmbeddingScorer::Create(
+          SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      Quantizer quantizer,
+      Quantizer::Create(/*float_min=*/-1.0f, /*float_max=*/1.0f));
+
+  int dimension = 3;
+  std::vector<float> v1 = {0.1f, 0.2f, 0.3f};
+  std::vector<float> v2 = {0.5f, 0.5f, 0.6f};
+  std::vector<uint8_t> v2_quantized = QuantizeVector(v2, quantizer);
+  float expected_dot_product = 0.1f * 0.5f + 0.2f * 0.5f + 0.3f * 0.6f;
+
+  // Test float computation
+  EXPECT_FLOAT_EQ(embedding_scorer->Score(dimension, v1.data(), v2.data()),
+                  expected_dot_product);
+
+  // Test quantization
+  EXPECT_NEAR(embedding_scorer->Score(dimension, v1.data(), v2_quantized.data(),
+                                      quantizer),
+              expected_dot_product, eps_quantized);
+}
+
+TEST(EmbeddingScorerTest, Cosine) {
+  constexpr float eps = 0.001f;
+  constexpr float eps_quantized = 0.01f;
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<EmbeddingScorer> embedding_scorer,
+      EmbeddingScorer::Create(
+          SearchSpecProto::EmbeddingQueryMetricType::COSINE));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      Quantizer quantizer,
+      Quantizer::Create(/*float_min=*/-1.0f, /*float_max=*/1.0f));
+
+  int dimension = 3;
+  std::vector<float> v1 = {0.7f, -0.3f, -0.6f};
+  std::vector<float> v2 = {-0.5f, 0.1f, -0.2f};
+  std::vector<uint8_t> v2_quantized = QuantizeVector(v2, quantizer);
+  float expected_cosine = -0.4896f;
+
+  // Test float computation
+  EXPECT_NEAR(embedding_scorer->Score(dimension, v1.data(), v2.data()),
+              expected_cosine, eps);
+
+  // Test quantization
+  EXPECT_NEAR(embedding_scorer->Score(dimension, v1.data(), v2_quantized.data(),
+                                      quantizer),
+              expected_cosine, eps_quantized);
+}
+
+TEST(EmbeddingScorerTest, Euclidean) {
+  constexpr float eps = 0.001f;
+  constexpr float eps_quantized = 0.01f;
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<EmbeddingScorer> embedding_scorer,
+      EmbeddingScorer::Create(
+          SearchSpecProto::EmbeddingQueryMetricType::EUCLIDEAN));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      Quantizer quantizer,
+      Quantizer::Create(/*float_min=*/-1.0f, /*float_max=*/1.0f));
+
+  int dimension = 3;
+  std::vector<float> v1 = {0.6f, -0.2f, 0.9f};
+  std::vector<float> v2 = {-0.8f, -0.4f, 0.2f};
+  std::vector<uint8_t> v2_quantized = QuantizeVector(v2, quantizer);
+  float expected_euclidean = 1.5780f;
+
+  // Test float computation
+  EXPECT_NEAR(embedding_scorer->Score(dimension, v1.data(), v2.data()),
+              expected_euclidean, eps);
+
+  // Test quantization
+  EXPECT_NEAR(embedding_scorer->Score(dimension, v1.data(), v2_quantized.data(),
+                                      quantizer),
+              expected_euclidean, eps_quantized);
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/index/embed/quantizer.h b/icing/index/embed/quantizer.h
new file mode 100644
index 0000000..099c96d
--- /dev/null
+++ b/icing/index/embed/quantizer.h
@@ -0,0 +1,91 @@
+// Copyright (C) 2024 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_QUANTIZER_H_
+#define ICING_INDEX_EMBED_QUANTIZER_H_
+
+#include <algorithm>
+#include <cmath>
+#include <cstdint>
+#include <cstring>
+#include <limits>
+
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/absl_ports/canonical_errors.h"
+
+namespace icing {
+namespace lib {
+
+// A class for quantizing and dequantizing floating-point values to and from
+// 8-bit unsigned integers. The maximum quantization error is
+// (float_max - float_min) / 255 / 2.
+class Quantizer {
+ public:
+  // Creates a new Quantizer instance based on the specified range. Values
+  // outside this range will be quantized to the closest boundary.
+  //
+  // Returns:
+  //   - An Quantizer instance on success.
+  //   - INVALID_ARGUMENT_ERROR if float_min is greater than or equal to
+  //     float_max.
+  static libtextclassifier3::StatusOr<Quantizer> Create(float float_min,
+                                                        float float_max) {
+    if (float_min > float_max) {
+      return absl_ports::InvalidArgumentError(
+          "float_min must be less than or equal to float_max.");
+    }
+    float scale_factor = 0.0;
+    if (float_max - float_min > kEpsilon) {  // Not equal.
+      scale_factor =
+          static_cast<float>(kMaxQuantizedValue) / (float_max - float_min);
+    }
+    return Quantizer(float_min, scale_factor);
+  }
+
+  // Creates a new Quantizer instance from the serialized data.
+  explicit Quantizer(const char* data) {
+    memcpy(this, data, sizeof(Quantizer));
+  }
+
+  uint8_t Quantize(float value) const {
+    double normalized =
+        (static_cast<double>(value) - float_min_) * scale_factor_;
+    double quantized = std::round(normalized);
+    quantized =
+        std::clamp(quantized, 0.0, static_cast<double>(kMaxQuantizedValue));
+    return static_cast<uint8_t>(quantized);
+  }
+
+  float Dequantize(uint8_t quantized) const {
+    if (scale_factor_ == 0.0) {
+      return float_min_;
+    }
+    return (quantized / scale_factor_) + float_min_;
+  }
+
+ private:
+  static constexpr uint8_t kMaxQuantizedValue =
+      std::numeric_limits<uint8_t>::max();
+  static constexpr float kEpsilon = 1e-6;
+
+  explicit Quantizer(float float_min, float scale_factor)
+      : float_min_(float_min), scale_factor_(scale_factor) {}
+  float float_min_;
+  float scale_factor_;
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_INDEX_EMBED_QUANTIZER_H_
diff --git a/icing/index/embed/quantizer_test.cc b/icing/index/embed/quantizer_test.cc
new file mode 100644
index 0000000..8b50bd5
--- /dev/null
+++ b/icing/index/embed/quantizer_test.cc
@@ -0,0 +1,171 @@
+// Copyright (C) 2024 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/quantizer.h"
+
+#include <cstdint>
+#include <limits>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "icing/testing/common-matchers.h"
+
+namespace icing {
+namespace lib {
+
+namespace {
+
+using ::testing::AnyOf;
+using ::testing::Eq;
+
+constexpr float kFloatEps = 1e-5f;
+
+constexpr float GetMaximumErrorForQuantization(float float_min,
+                                               float float_max) {
+  return (float_max - float_min) / 255 / 2 + kFloatEps;
+}
+
+TEST(QuantizerTest, CreateFailure) {
+  EXPECT_THAT(Quantizer::Create(/*float_min=*/1.0f, /*float_max=*/0.0f),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+TEST(QuantizerTest, QuantizeAndDequantize) {
+  constexpr float float_min = -1.0f;
+  constexpr float float_max = 1.0f;
+  constexpr float eps = GetMaximumErrorForQuantization(float_min, float_max);
+
+  ICING_ASSERT_OK_AND_ASSIGN(Quantizer quantizer,
+                             Quantizer::Create(float_min, float_max));
+
+  // float_min
+  EXPECT_EQ(quantizer.Quantize(float_min), 0);
+  EXPECT_FLOAT_EQ(quantizer.Dequantize(0), float_min);
+
+  // float_max
+  EXPECT_EQ(quantizer.Quantize(float_max), 255);
+  EXPECT_FLOAT_EQ(quantizer.Dequantize(255), float_max);
+
+  // Midpoint
+  // After scaling the midpoint value, we should get 127.5. Due to float
+  // precision, the quantized value can be either 127 or 128.
+  float original_value = 0.0f;
+  uint8_t quantized_value = quantizer.Quantize(original_value);
+  EXPECT_THAT(quantized_value, AnyOf(Eq(127), Eq(128)));
+  EXPECT_NEAR(quantizer.Dequantize(quantized_value), original_value, eps);
+
+  // Other values
+  original_value = -0.5f;
+  quantized_value = quantizer.Quantize(original_value);
+  EXPECT_EQ(quantized_value, 64);
+  EXPECT_NEAR(quantizer.Dequantize(quantized_value), original_value, eps);
+  original_value = 0.5f;
+  quantized_value = quantizer.Quantize(original_value);
+  EXPECT_EQ(quantized_value, 191);
+  EXPECT_NEAR(quantizer.Dequantize(quantized_value), original_value, eps);
+
+  // Out of range values
+  EXPECT_EQ(quantizer.Quantize(-2.0f), 0);
+  EXPECT_EQ(quantizer.Quantize(2.0f), 255);
+  EXPECT_EQ(quantizer.Quantize(std::numeric_limits<float>::lowest()), 0);
+  EXPECT_EQ(quantizer.Quantize(std::numeric_limits<float>::max()), 255);
+}
+
+TEST(QuantizerTest, QuantizeAndDequantizeLargerRange) {
+  constexpr float float_min = -100.0f;
+  constexpr float float_max = 100.0f;
+  constexpr float eps = GetMaximumErrorForQuantization(float_min, float_max);
+
+  ICING_ASSERT_OK_AND_ASSIGN(Quantizer quantizer,
+                             Quantizer::Create(float_min, float_max));
+
+  // float_min
+  EXPECT_EQ(quantizer.Quantize(float_min), 0);
+  EXPECT_FLOAT_EQ(quantizer.Dequantize(0), float_min);
+
+  // float_max
+  EXPECT_EQ(quantizer.Quantize(float_max), 255);
+  EXPECT_FLOAT_EQ(quantizer.Dequantize(255), float_max);
+
+  // Midpoint
+  // After scaling the midpoint value, we should get 127.5. Due to float
+  // precision, the quantized value can be either 127 or 128.
+  float original_value = 0.0f;
+  uint8_t quantized_value = quantizer.Quantize(original_value);
+  EXPECT_THAT(quantized_value, AnyOf(Eq(127), Eq(128)));
+  EXPECT_NEAR(quantizer.Dequantize(quantized_value), original_value, eps);
+
+  // Other values
+  original_value = -50.0f;
+  quantized_value = quantizer.Quantize(original_value);
+  EXPECT_EQ(quantized_value, 64);
+  EXPECT_NEAR(quantizer.Dequantize(quantized_value), original_value, eps);
+  original_value = 50.0f;
+  quantized_value = quantizer.Quantize(original_value);
+  EXPECT_EQ(quantized_value, 191);
+  EXPECT_NEAR(quantizer.Dequantize(quantized_value), original_value, eps);
+
+  // Out of range values
+  EXPECT_EQ(quantizer.Quantize(-150.0f), 0);
+  EXPECT_EQ(quantizer.Quantize(150.0f), 255);
+  EXPECT_EQ(quantizer.Quantize(std::numeric_limits<float>::lowest()), 0);
+  EXPECT_EQ(quantizer.Quantize(std::numeric_limits<float>::max()), 255);
+}
+
+// Test when the float range is equal to the uint8_t range. In this case,
+// Quantize and Dequantize should be identity functions.
+TEST(QuantizerTest, QuantizeAndDequantizeEqualRange) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      Quantizer quantizer,
+      Quantizer::Create(/*float_min=*/0.0f, /*float_max=*/255.0f));
+
+  for (int i = 0; i < 256; ++i) {
+    EXPECT_EQ(quantizer.Quantize(static_cast<float>(i)), i);
+    EXPECT_FLOAT_EQ(quantizer.Dequantize(i), static_cast<float>(i));
+  }
+
+  // Out of range values
+  EXPECT_EQ(quantizer.Quantize(-300.0f), 0);
+  EXPECT_EQ(quantizer.Quantize(300.0f), 255);
+  EXPECT_EQ(quantizer.Quantize(std::numeric_limits<float>::lowest()), 0);
+  EXPECT_EQ(quantizer.Quantize(std::numeric_limits<float>::max()), 255);
+}
+
+TEST(QuantizerTest, QuantizeAndDequantizeWithMinEqualToMax) {
+  float constant_values[] = {0.0f, 1.0f, -1.0f, 5.0f, -5.0f, 100.0f, -100.0f};
+  for (float constant_value : constant_values) {
+    ICING_ASSERT_OK_AND_ASSIGN(Quantizer quantizer,
+                               Quantizer::Create(/*float_min=*/constant_value,
+                                                 /*float_max=*/constant_value));
+
+    // All values should be quantized to 0.
+    for (float float_value = constant_value - 5;
+         float_value <= constant_value + 5; ++float_value) {
+      EXPECT_EQ(quantizer.Quantize(float_value), 0);
+    }
+
+    // All quantized values should be dequantized to the constant value.
+    for (int quantized_value = 0;
+         quantized_value <= std::numeric_limits<uint8_t>::max();
+         ++quantized_value) {
+      EXPECT_FLOAT_EQ(quantizer.Dequantize(quantized_value), constant_value);
+    }
+  }
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/index/embedding-indexing-handler.cc b/icing/index/embedding-indexing-handler.cc
index 049e307..28d6568 100644
--- a/icing/index/embedding-indexing-handler.cc
+++ b/icing/index/embedding-indexing-handler.cc
@@ -33,17 +33,19 @@
 
 libtextclassifier3::StatusOr<std::unique_ptr<EmbeddingIndexingHandler>>
 EmbeddingIndexingHandler::Create(const Clock* clock,
-                                 EmbeddingIndex* embedding_index) {
+                                 EmbeddingIndex* embedding_index,
+                                 bool enable_embedding_index) {
   ICING_RETURN_ERROR_IF_NULL(clock);
   ICING_RETURN_ERROR_IF_NULL(embedding_index);
 
-  return std::unique_ptr<EmbeddingIndexingHandler>(
-      new EmbeddingIndexingHandler(clock, embedding_index));
+  return std::unique_ptr<EmbeddingIndexingHandler>(new EmbeddingIndexingHandler(
+      clock, embedding_index, enable_embedding_index));
 }
 
 libtextclassifier3::Status EmbeddingIndexingHandler::Handle(
     const TokenizedDocument& tokenized_document, DocumentId document_id,
-    bool recovery_mode, PutDocumentStatsProto* put_document_stats) {
+    DocumentId /*old_document_id*/ _, bool recovery_mode,
+    PutDocumentStatsProto* put_document_stats) {
   std::unique_ptr<Timer> index_timer = clock_.GetNewTimer();
 
   if (!IsDocumentIdValid(document_id)) {
@@ -64,18 +66,21 @@
   }
   embedding_index_.set_last_added_document_id(document_id);
 
-  for (const Section<PropertyProto::VectorProto>& vector_section :
-       tokenized_document.vector_sections()) {
-    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));
+  if (enable_embedding_index_) {
+    for (const Section<PropertyProto::VectorProto>& vector_section :
+         tokenized_document.vector_sections()) {
+      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));
+      }
     }
-  }
-  ICING_RETURN_IF_ERROR(embedding_index_.CommitBufferToIndex());
+    ICING_RETURN_IF_ERROR(embedding_index_.CommitBufferToIndex());
 
-  if (put_document_stats != nullptr) {
-    put_document_stats->set_embedding_index_latency_ms(
-        index_timer->GetElapsedMilliseconds());
+    if (put_document_stats != nullptr) {
+      put_document_stats->set_embedding_index_latency_ms(
+          index_timer->GetElapsedMilliseconds());
+    }
   }
 
   return libtextclassifier3::Status::OK;
diff --git a/icing/index/embedding-indexing-handler.h b/icing/index/embedding-indexing-handler.h
index f3adf6a..6bbca42 100644
--- a/icing/index/embedding-indexing-handler.h
+++ b/icing/index/embedding-indexing-handler.h
@@ -40,11 +40,15 @@
   //   - An EmbeddingIndexingHandler instance on success
   //   - FAILED_PRECONDITION_ERROR if any of the input pointer is null
   static libtextclassifier3::StatusOr<std::unique_ptr<EmbeddingIndexingHandler>>
-  Create(const Clock* clock, EmbeddingIndex* embedding_index);
+  Create(const Clock* clock, EmbeddingIndex* embedding_index,
+         bool enable_embedding_index);
 
   // Handles the embedding indexing process: add hits into the embedding index
   // for all contents in tokenized_document.vector_sections.
   //
+  // Parameter old_document_id is unused since there is no need to migrate data
+  // from old_document_id to (new) document_id.
+  //
   // Returns:
   //   - OK on success.
   //   - INVALID_ARGUMENT_ERROR if document_id is invalid OR document_id is less
@@ -54,14 +58,19 @@
   //   - Any embedding index errors.
   libtextclassifier3::Status Handle(
       const TokenizedDocument& tokenized_document, DocumentId document_id,
-      bool recovery_mode, PutDocumentStatsProto* put_document_stats) override;
+      DocumentId /*old_document_id*/ _, bool recovery_mode,
+      PutDocumentStatsProto* put_document_stats) override;
 
  private:
   explicit EmbeddingIndexingHandler(const Clock* clock,
-                                    EmbeddingIndex* embedding_index)
-      : DataIndexingHandler(clock), embedding_index_(*embedding_index) {}
+                                    EmbeddingIndex* embedding_index,
+                                    bool enable_embedding_index)
+      : DataIndexingHandler(clock),
+        embedding_index_(*embedding_index),
+        enable_embedding_index_(enable_embedding_index) {}
 
   EmbeddingIndex& embedding_index_;
+  bool enable_embedding_index_;
 };
 
 }  // namespace lib
diff --git a/icing/index/embedding-indexing-handler_test.cc b/icing/index/embedding-indexing-handler_test.cc
index a1b3ea8..5cfbc87 100644
--- a/icing/index/embedding-indexing-handler_test.cc
+++ b/icing/index/embedding-indexing-handler_test.cc
@@ -14,25 +14,22 @@
 
 #include "icing/index/embedding-indexing-handler.h"
 
-#include <cstdint>
 #include <initializer_list>
 #include <memory>
 #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 "gmock/gmock.h"
 #include "gtest/gtest.h"
-#include "icing/absl_ports/canonical_errors.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/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/quantizer.h"
 #include "icing/index/hit/hit.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document_wrapper.pb.h"
@@ -45,12 +42,12 @@
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/embedding-test-utils.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.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/status-macros.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "icing/util/tokenized-document.h"
 #include "unicode/uloc.h"
 
@@ -61,49 +58,60 @@
 
 using ::testing::ElementsAre;
 using ::testing::Eq;
+using ::testing::FloatNear;
 using ::testing::IsEmpty;
 using ::testing::IsTrue;
+using ::testing::Pointwise;
 
 // Indexable properties (section) and section id. Section id is determined by
 // the lexicographical order of indexable property paths.
 // Schema type with indexable properties: FakeType
 // Section id = 0: "body"
 // Section id = 1: "bodyEmbedding"
-// Section id = 2: "title"
-// Section id = 3: "titleEmbedding"
+// Section id = 2: "quantizedEmbedding"
+// Section id = 3: "title"
+// Section id = 4: "titleEmbedding"
 static constexpr std::string_view kFakeType = "FakeType";
 static constexpr std::string_view kPropertyBody = "body";
 static constexpr std::string_view kPropertyBodyEmbedding = "bodyEmbedding";
+static constexpr std::string_view kPropertyQuantizedEmbedding =
+    "quantizedEmbedding";
 static constexpr std::string_view kPropertyTitle = "title";
 static constexpr std::string_view kPropertyTitleEmbedding = "titleEmbedding";
 static constexpr std::string_view kPropertyNonIndexableEmbedding =
     "nonIndexableEmbedding";
 
 static constexpr SectionId kSectionIdBodyEmbedding = 1;
-static constexpr SectionId kSectionIdTitleEmbedding = 3;
+static constexpr SectionId kSectionIdQuantizedEmbedding = 2;
+static constexpr SectionId kSectionIdTitleEmbedding = 4;
 
 // Schema type with nested indexable properties: FakeCollectionType
 // Section id = 0: "collection.body"
 // Section id = 1: "collection.bodyEmbedding"
-// Section id = 2: "collection.title"
-// Section id = 3: "collection.titleEmbedding"
-// Section id = 4: "fullDocEmbedding"
+// Section id = 2: "collection.quantizedEmbedding"
+// Section id = 3: "collection.title"
+// Section id = 4: "collection.titleEmbedding"
+// Section id = 5: "fullDocEmbedding"
 static constexpr std::string_view kFakeCollectionType = "FakeCollectionType";
 static constexpr std::string_view kPropertyCollection = "collection";
 static constexpr std::string_view kPropertyFullDocEmbedding =
     "fullDocEmbedding";
 
 static constexpr SectionId kSectionIdNestedBodyEmbedding = 1;
-static constexpr SectionId kSectionIdNestedTitleEmbedding = 3;
-static constexpr SectionId kSectionIdFullDocEmbedding = 4;
+static constexpr SectionId kSectionIdNestedQuantizedEmbedding = 2;
+static constexpr SectionId kSectionIdNestedTitleEmbedding = 4;
+static constexpr SectionId kSectionIdFullDocEmbedding = 5;
+
+constexpr float kEpsQuantized = 0.01f;
 
 class EmbeddingIndexingHandlerTest : public ::testing::Test {
  protected:
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
 
@@ -115,10 +123,6 @@
     schema_store_dir_ = base_dir_ + "/schema_store";
     document_store_dir_ = base_dir_ + "/document_store";
 
-    ICING_ASSERT_OK_AND_ASSIGN(
-        embedding_index_,
-        EmbeddingIndex::Create(&filesystem_, embedding_index_working_path_));
-
     language_segmenter_factory::SegmenterOptions segmenter_options(ULOC_US);
     ICING_ASSERT_OK_AND_ASSIGN(
         lang_segmenter_,
@@ -128,8 +132,8 @@
         filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str()),
         IsTrue());
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     SchemaProto schema =
         SchemaBuilder()
             .AddType(
@@ -159,6 +163,14 @@
                                 EmbeddingIndexingConfig::EmbeddingIndexingType::
                                     LINEAR_SEARCH)
                             .SetCardinality(CARDINALITY_REPEATED))
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName(kPropertyQuantizedEmbedding)
+                            .SetDataTypeVector(
+                                EmbeddingIndexingConfig::EmbeddingIndexingType::
+                                    LINEAR_SEARCH,
+                                QUANTIZATION_TYPE_QUANTIZE_8_BIT)
+                            .SetCardinality(CARDINALITY_REPEATED))
                     .AddProperty(PropertyConfigBuilder()
                                      .SetName(kPropertyNonIndexableEmbedding)
                                      .SetDataType(TYPE_VECTOR)
@@ -188,15 +200,19 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult doc_store_create_result,
         DocumentStore::Create(&filesystem_, document_store_dir_, &fake_clock_,
-                              schema_store_.get(),
+                              schema_store_.get(), feature_flags_.get(),
                               /*force_recovery_and_revalidate_documents=*/false,
-                              /*namespace_id_fingerprint=*/true,
                               /*pre_mapping_fbv=*/false,
                               /*use_persistent_hash_map=*/true,
                               PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDeflateCompressionLevel,
+                                  DocumentWrapper>::kDefaultCompressionLevel,
                               /*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()));
   }
 
   void TearDown() override {
@@ -208,43 +224,7 @@
     filesystem_.DeleteDirectoryRecursively(base_dir_.c_str());
   }
 
-  libtextclassifier3::StatusOr<std::vector<EmbeddingHit>> GetHits(
-      uint32_t dimension, std::string_view model_signature) {
-    std::vector<EmbeddingHit> hits;
-
-    libtextclassifier3::StatusOr<
-        std::unique_ptr<PostingListEmbeddingHitAccessor>>
-        pl_accessor_or =
-            embedding_index_->GetAccessor(dimension, model_signature);
-    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())) {
-      return hits;
-    } else {
-      return std::move(pl_accessor_or).status();
-    }
-
-    while (true) {
-      ICING_ASSIGN_OR_RETURN(std::vector<EmbeddingHit> batch,
-                             pl_accessor->GetNextHitsBatch());
-      if (batch.empty()) {
-        return hits;
-      }
-      hits.insert(hits.end(), batch.begin(), batch.end());
-    }
-  }
-
-  std::vector<float> GetRawEmbeddingData() {
-    auto data_or = embedding_index_->GetRawEmbeddingData();
-    if (!data_or.ok()) {
-      return std::vector<float>();
-    }
-    return std::vector<float>(
-        data_or.ValueOrDie(),
-        data_or.ValueOrDie() + embedding_index_->GetTotalVectorSize());
-  }
-
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   FakeClock fake_clock_;
   std::string base_dir_;
@@ -262,11 +242,13 @@
 
 TEST_F(EmbeddingIndexingHandlerTest, CreationWithNullPointerShouldFail) {
   EXPECT_THAT(EmbeddingIndexingHandler::Create(/*clock=*/nullptr,
-                                               embedding_index_.get()),
+                                               embedding_index_.get(),
+                                               /*enable_embedding_index=*/true),
               StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 
   EXPECT_THAT(EmbeddingIndexingHandler::Create(&fake_clock_,
-                                               /*embedding_index=*/nullptr),
+                                               /*embedding_index=*/nullptr,
+                                               /*enable_embedding_index=*/true),
               StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 }
 
@@ -282,6 +264,84 @@
           .AddVectorProperty(std::string(kPropertyBodyEmbedding),
                              CreateVector("model", {0.4, 0.5, 0.6}),
                              CreateVector("model", {0.7, 0.8, 0.9}))
+          .AddVectorProperty(std::string(kPropertyQuantizedEmbedding),
+                             CreateVector("model", {0.1, 0.2, 0.3}),
+                             CreateVector("model", {0.4, 0.5, 0.6}))
+          .AddVectorProperty(std::string(kPropertyNonIndexableEmbedding),
+                             CreateVector("model", {1.1, 1.2, 1.3}))
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_document,
+      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
+                                std::move(document)));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result,
+      document_store_->Put(tokenized_document.document()));
+  DocumentId document_id = put_result.new_document_id;
+
+  ASSERT_THAT(embedding_index_->last_added_document_id(),
+              Eq(kInvalidDocumentId));
+  // Handle document.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<EmbeddingIndexingHandler> handler,
+      EmbeddingIndexingHandler::Create(&fake_clock_, embedding_index_.get(),
+                                       /*enable_embedding_index=*/true));
+  EXPECT_THAT(handler->Handle(
+                  tokenized_document, document_id, put_result.old_document_id,
+                  /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
+              IsOk());
+
+  // Check index
+  EmbeddingHit hit1(BasicHit(kSectionIdBodyEmbedding, /*document_id=*/0),
+                    /*location=*/0);
+  EmbeddingHit hit2(BasicHit(kSectionIdBodyEmbedding, /*document_id=*/0),
+                    /*location=*/3);
+  EmbeddingHit hit3(BasicHit(kSectionIdTitleEmbedding, /*document_id=*/0),
+                    /*location=*/6);
+  // Quantized embeddings are stored in a different location from unquantized
+  // embeddings, so the location starts from 0 again.
+  EmbeddingHit quantized_hit1(
+      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/0),
+      /*location=*/0);
+  EmbeddingHit quantized_hit2(
+      BasicHit(kSectionIdQuantizedEmbedding, /*document_id=*/0),
+      /*location=*/3 + sizeof(Quantizer));
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
+              IsOkAndHolds(ElementsAre(hit1, hit2, quantized_hit1,
+                                       quantized_hit2, hit3)));
+  // Check unquantized embedding data
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+              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(),
+              Eq(6 + 2 * sizeof(Quantizer)));
+  EXPECT_THAT(
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
+                                                     quantized_hit1,
+                                                     /*dimension=*/3),
+      IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.1, 0.2, 0.3})));
+  EXPECT_THAT(
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
+                                                     quantized_hit2,
+                                                     /*dimension=*/3),
+      IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.4, 0.5, 0.6})));
+
+  EXPECT_THAT(embedding_index_->last_added_document_id(), Eq(document_id));
+}
+
+TEST_F(EmbeddingIndexingHandlerTest, EmbeddingShouldNotBeIndexedIfDisabled) {
+  DocumentProto document =
+      DocumentBuilder()
+          .SetKey("icing", "fake_type/1")
+          .SetSchema(std::string(kFakeType))
+          .AddStringProperty(std::string(kPropertyTitle), "title")
+          .AddVectorProperty(std::string(kPropertyTitleEmbedding),
+                             CreateVector("model", {0.1, 0.2, 0.3}))
+          .AddStringProperty(std::string(kPropertyBody), "body")
+          .AddVectorProperty(std::string(kPropertyBodyEmbedding),
+                             CreateVector("model", {0.4, 0.5, 0.6}),
+                             CreateVector("model", {0.7, 0.8, 0.9}))
           .AddVectorProperty(std::string(kPropertyNonIndexableEmbedding),
                              CreateVector("model", {1.1, 1.2, 1.3}))
           .Build();
@@ -296,27 +356,22 @@
 
   ASSERT_THAT(embedding_index_->last_added_document_id(),
               Eq(kInvalidDocumentId));
-  // Handle document.
+  // If enable_embedding_index is false, the handler should not index any
+  // embeddings.
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<EmbeddingIndexingHandler> handler,
-      EmbeddingIndexingHandler::Create(&fake_clock_, embedding_index_.get()));
-  EXPECT_THAT(
-      handler->Handle(tokenized_document, document_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
-      IsOk());
+      EmbeddingIndexingHandler::Create(&fake_clock_, embedding_index_.get(),
+                                       /*enable_embedding_index=*/false));
+  EXPECT_THAT(handler->Handle(
+                  tokenized_document, document_id, put_result.old_document_id,
+                  /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
+              IsOk());
 
-  // Check index
-  EXPECT_THAT(
-      GetHits(/*dimension=*/3, /*model_signature=*/"model"),
-      IsOkAndHolds(ElementsAre(
-          EmbeddingHit(BasicHit(kSectionIdBodyEmbedding, /*document_id=*/0),
-                       /*location=*/0),
-          EmbeddingHit(BasicHit(kSectionIdBodyEmbedding, /*document_id=*/0),
-                       /*location=*/3),
-          EmbeddingHit(BasicHit(kSectionIdTitleEmbedding, /*document_id=*/0),
-                       /*location=*/6))));
-  EXPECT_THAT(GetRawEmbeddingData(),
-              ElementsAre(0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1, 0.2, 0.3));
+  // Check that the embedding index is empty.
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
+              IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
   EXPECT_THAT(embedding_index_->last_added_document_id(), Eq(document_id));
 }
 
@@ -337,6 +392,9 @@
                   .AddVectorProperty(std::string(kPropertyBodyEmbedding),
                                      CreateVector("model", {0.4, 0.5, 0.6}),
                                      CreateVector("model", {0.7, 0.8, 0.9}))
+                  .AddVectorProperty(std::string(kPropertyQuantizedEmbedding),
+                                     CreateVector("model", {0.1, 0.2, 0.3}),
+                                     CreateVector("model", {0.4, 0.5, 0.6}))
                   .AddVectorProperty(
                       std::string(kPropertyNonIndexableEmbedding),
                       CreateVector("model", {1.1, 1.2, 1.3}))
@@ -358,34 +416,57 @@
   // Handle document.
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<EmbeddingIndexingHandler> handler,
-      EmbeddingIndexingHandler::Create(&fake_clock_, embedding_index_.get()));
-  EXPECT_THAT(
-      handler->Handle(tokenized_document, document_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
-      IsOk());
+      EmbeddingIndexingHandler::Create(&fake_clock_, embedding_index_.get(),
+                                       /*enable_embedding_index=*/true));
+  EXPECT_THAT(handler->Handle(
+                  tokenized_document, document_id, put_result.old_document_id,
+                  /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
+              IsOk());
 
   // Check index
+  EmbeddingHit hit1(BasicHit(kSectionIdNestedBodyEmbedding, /*document_id=*/0),
+                    /*location=*/0);
+  EmbeddingHit hit2(BasicHit(kSectionIdNestedBodyEmbedding, /*document_id=*/0),
+                    /*location=*/3);
+  EmbeddingHit hit3(BasicHit(kSectionIdNestedTitleEmbedding, /*document_id=*/0),
+                    /*location=*/6);
+  EmbeddingHit hit4(BasicHit(kSectionIdFullDocEmbedding, /*document_id=*/0),
+                    /*location=*/9);
+  // Quantized embeddings are stored in a different location from unquantized
+  // embeddings, so the location starts from 0 again.
+  EmbeddingHit quantized_hit1(
+      BasicHit(kSectionIdNestedQuantizedEmbedding, /*document_id=*/0),
+      /*location=*/0);
+  EmbeddingHit quantized_hit2(
+      BasicHit(kSectionIdNestedQuantizedEmbedding, /*document_id=*/0),
+      /*location=*/3 + sizeof(Quantizer));
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
+              IsOkAndHolds(ElementsAre(hit1, hit2, quantized_hit1,
+                                       quantized_hit2, hit3, hit4)));
+  // Check unquantized embedding data
   EXPECT_THAT(
-      GetHits(/*dimension=*/3, /*model_signature=*/"model"),
-      IsOkAndHolds(ElementsAre(
-          EmbeddingHit(
-              BasicHit(kSectionIdNestedBodyEmbedding, /*document_id=*/0),
-              /*location=*/0),
-          EmbeddingHit(
-              BasicHit(kSectionIdNestedBodyEmbedding, /*document_id=*/0),
-              /*location=*/3),
-          EmbeddingHit(
-              BasicHit(kSectionIdNestedTitleEmbedding, /*document_id=*/0),
-              /*location=*/6),
-          EmbeddingHit(BasicHit(kSectionIdFullDocEmbedding, /*document_id=*/0),
-                       /*location=*/9))));
-  EXPECT_THAT(GetRawEmbeddingData(), 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));
+      GetRawEmbeddingDataFromIndex(embedding_index_.get()),
+      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(),
+              Eq(6 + 2 * sizeof(Quantizer)));
+  EXPECT_THAT(
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
+                                                     quantized_hit1,
+                                                     /*dimension=*/3),
+      IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.1, 0.2, 0.3})));
+  EXPECT_THAT(
+      GetAndRestoreQuantizedEmbeddingVectorFromIndex(embedding_index_.get(),
+                                                     quantized_hit2,
+                                                     /*dimension=*/3),
+      IsOkAndHolds(Pointwise(FloatNear(kEpsQuantized), {0.4, 0.5, 0.6})));
+
   EXPECT_THAT(embedding_index_->last_added_document_id(), Eq(document_id));
 }
 
 TEST_F(EmbeddingIndexingHandlerTest,
-       HandleInvalidDocumentIdShouldReturnInvalidArgumentError) {
+       HandleInvalidNewDocumentIdShouldReturnInvalidArgumentError) {
   DocumentProto document =
       DocumentBuilder()
           .SetKey("icing", "fake_type/1")
@@ -413,34 +494,39 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<EmbeddingIndexingHandler> handler,
-      EmbeddingIndexingHandler::Create(&fake_clock_, embedding_index_.get()));
+      EmbeddingIndexingHandler::Create(&fake_clock_, embedding_index_.get(),
+                                       /*enable_embedding_index=*/true));
 
   // Handling document with kInvalidDocumentId should cause a failure, and both
   // index data and last_added_document_id should remain unchanged.
   EXPECT_THAT(
       handler->Handle(tokenized_document, kInvalidDocumentId,
+                      /*old_document_id=*/kInvalidDocumentId,
                       /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(embedding_index_->last_added_document_id(),
               Eq(kCurrentDocumentId));
   // Check that the embedding index should be empty
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
               IsOkAndHolds(IsEmpty()));
   EXPECT_TRUE(embedding_index_->is_empty());
-  EXPECT_THAT(GetRawEmbeddingData(), IsEmpty());
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
 
   // Recovery mode should get the same result.
   EXPECT_THAT(
       handler->Handle(tokenized_document, kInvalidDocumentId,
+                      /*old_document_id=*/kInvalidDocumentId,
                       /*recovery_mode=*/true, /*put_document_stats=*/nullptr),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(embedding_index_->last_added_document_id(),
               Eq(kCurrentDocumentId));
   // Check that the embedding index should be empty
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
               IsOkAndHolds(IsEmpty()));
   EXPECT_TRUE(embedding_index_->is_empty());
-  EXPECT_THAT(GetRawEmbeddingData(), IsEmpty());
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
 }
 
 TEST_F(EmbeddingIndexingHandlerTest,
@@ -470,41 +556,44 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<EmbeddingIndexingHandler> handler,
-      EmbeddingIndexingHandler::Create(&fake_clock_, embedding_index_.get()));
+      EmbeddingIndexingHandler::Create(&fake_clock_, embedding_index_.get(),
+                                       /*enable_embedding_index=*/true));
 
   // Handling document with document_id == last_added_document_id should cause a
   // failure, and both index data and last_added_document_id should remain
   // unchanged.
   embedding_index_->set_last_added_document_id(document_id);
   ASSERT_THAT(embedding_index_->last_added_document_id(), Eq(document_id));
-  EXPECT_THAT(
-      handler->Handle(tokenized_document, document_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
-      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(handler->Handle(
+                  tokenized_document, document_id, put_result.old_document_id,
+                  /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(embedding_index_->last_added_document_id(), Eq(document_id));
 
   // Check that the embedding index should be empty
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
               IsOkAndHolds(IsEmpty()));
   EXPECT_TRUE(embedding_index_->is_empty());
-  EXPECT_THAT(GetRawEmbeddingData(), IsEmpty());
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), 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
   // unchanged.
   embedding_index_->set_last_added_document_id(document_id + 1);
   ASSERT_THAT(embedding_index_->last_added_document_id(), Eq(document_id + 1));
-  EXPECT_THAT(
-      handler->Handle(tokenized_document, document_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
-      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(handler->Handle(
+                  tokenized_document, document_id, put_result.old_document_id,
+                  /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(embedding_index_->last_added_document_id(), Eq(document_id + 1));
 
   // Check that the embedding index should be empty
-  EXPECT_THAT(GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+  EXPECT_THAT(GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                        /*model_signature=*/"model"),
               IsOkAndHolds(IsEmpty()));
   EXPECT_TRUE(embedding_index_->is_empty());
-  EXPECT_THAT(GetRawEmbeddingData(), IsEmpty());
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()), IsEmpty());
 }
 
 TEST_F(EmbeddingIndexingHandlerTest,
@@ -556,19 +645,22 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<EmbeddingIndexingHandler> handler,
-      EmbeddingIndexingHandler::Create(&fake_clock_, embedding_index_.get()));
+      EmbeddingIndexingHandler::Create(&fake_clock_, embedding_index_.get(),
+                                       /*enable_embedding_index=*/true));
 
   // Handle document with document_id > last_added_document_id in recovery mode.
   // The handler should index this document and update last_added_document_id.
   EXPECT_THAT(
-      handler->Handle(tokenized_document1, document_id1, /*recovery_mode=*/true,
+      handler->Handle(tokenized_document1, document_id1,
+                      put_result1.old_document_id, /*recovery_mode=*/true,
                       /*put_document_stats=*/nullptr),
       IsOk());
   EXPECT_THAT(embedding_index_->last_added_document_id(), Eq(document_id1));
 
   // Check index
   EXPECT_THAT(
-      GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+      GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                /*model_signature=*/"model"),
       IsOkAndHolds(ElementsAre(
           EmbeddingHit(BasicHit(kSectionIdBodyEmbedding, /*document_id=*/0),
                        /*location=*/0),
@@ -576,7 +668,7 @@
                        /*location=*/3),
           EmbeddingHit(BasicHit(kSectionIdTitleEmbedding, /*document_id=*/0),
                        /*location=*/6))));
-  EXPECT_THAT(GetRawEmbeddingData(),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
               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
@@ -586,14 +678,16 @@
   embedding_index_->set_last_added_document_id(document_id2);
   ASSERT_THAT(embedding_index_->last_added_document_id(), Eq(document_id2));
   EXPECT_THAT(
-      handler->Handle(tokenized_document2, document_id2, /*recovery_mode=*/true,
+      handler->Handle(tokenized_document2, document_id2,
+                      put_result2.old_document_id, /*recovery_mode=*/true,
                       /*put_document_stats=*/nullptr),
       IsOk());
   EXPECT_THAT(embedding_index_->last_added_document_id(), Eq(document_id2));
 
   // Check index
   EXPECT_THAT(
-      GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+      GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                /*model_signature=*/"model"),
       IsOkAndHolds(ElementsAre(
           EmbeddingHit(BasicHit(kSectionIdBodyEmbedding, /*document_id=*/0),
                        /*location=*/0),
@@ -601,7 +695,7 @@
                        /*location=*/3),
           EmbeddingHit(BasicHit(kSectionIdTitleEmbedding, /*document_id=*/0),
                        /*location=*/6))));
-  EXPECT_THAT(GetRawEmbeddingData(),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
               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.
@@ -610,14 +704,16 @@
   embedding_index_->set_last_added_document_id(document_id2 + 1);
   ASSERT_THAT(embedding_index_->last_added_document_id(), Eq(document_id2 + 1));
   EXPECT_THAT(
-      handler->Handle(tokenized_document2, document_id2, /*recovery_mode=*/true,
+      handler->Handle(tokenized_document2, document_id2,
+                      put_result2.old_document_id, /*recovery_mode=*/true,
                       /*put_document_stats=*/nullptr),
       IsOk());
   EXPECT_THAT(embedding_index_->last_added_document_id(), Eq(document_id2 + 1));
 
   // Check index
   EXPECT_THAT(
-      GetHits(/*dimension=*/3, /*model_signature=*/"model"),
+      GetEmbeddingHitsFromIndex(embedding_index_.get(), /*dimension=*/3,
+                                /*model_signature=*/"model"),
       IsOkAndHolds(ElementsAre(
           EmbeddingHit(BasicHit(kSectionIdBodyEmbedding, /*document_id=*/0),
                        /*location=*/0),
@@ -625,7 +721,7 @@
                        /*location=*/3),
           EmbeddingHit(BasicHit(kSectionIdTitleEmbedding, /*document_id=*/0),
                        /*location=*/6))));
-  EXPECT_THAT(GetRawEmbeddingData(),
+  EXPECT_THAT(GetRawEmbeddingDataFromIndex(embedding_index_.get()),
               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.cc b/icing/index/index-processor.cc
index 9a773e8..6ed270f 100644
--- a/icing/index/index-processor.cc
+++ b/icing/index/index-processor.cc
@@ -28,12 +28,13 @@
 
 libtextclassifier3::Status IndexProcessor::IndexDocument(
     const TokenizedDocument& tokenized_document, DocumentId document_id,
-    PutDocumentStatsProto* put_document_stats) {
+    DocumentId old_document_id, PutDocumentStatsProto* put_document_stats) {
   std::unique_ptr<Timer> index_timer = clock_.GetNewTimer();
 
   for (auto& data_indexing_handler : data_indexing_handlers_) {
     ICING_RETURN_IF_ERROR(data_indexing_handler->Handle(
-        tokenized_document, document_id, recovery_mode_, put_document_stats));
+        tokenized_document, document_id, old_document_id, recovery_mode_,
+        put_document_stats));
   }
 
   if (put_document_stats != nullptr) {
diff --git a/icing/index/index-processor.h b/icing/index/index-processor.h
index 9b96f00..1eb8b55 100644
--- a/icing/index/index-processor.h
+++ b/icing/index/index-processor.h
@@ -15,14 +15,15 @@
 #ifndef ICING_INDEX_INDEX_PROCESSOR_H_
 #define ICING_INDEX_INDEX_PROCESSOR_H_
 
-#include <cstdint>
 #include <memory>
+#include <utility>
 #include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/index/data-indexing-handler.h"
 #include "icing/proto/logging.pb.h"
 #include "icing/store/document-id.h"
+#include "icing/util/clock.h"
 #include "icing/util/tokenized-document.h"
 
 namespace icing {
@@ -42,6 +43,12 @@
   // the first max_tokens_per_document will be added to the index. All tokens of
   // length exceeding max_token_length will be shortened to max_token_length.
   //
+  // old_document_id is provided. If valid, then it means the document with
+  // the same (namespace, uri) exists previously, and it is updated with new
+  // contents at this round. Each indexing handler should decide whether
+  // migrating existing data from old_document_id to (new) document_id according
+  // to each index's data logic.
+  //
   // Indexing a document *may* trigger an index merge. If a merge fails, then
   // all content in the index will be lost.
   //
@@ -53,6 +60,7 @@
   //   - Any DataIndexingHandler errors.
   libtextclassifier3::Status IndexDocument(
       const TokenizedDocument& tokenized_document, DocumentId document_id,
+      DocumentId old_document_id,
       PutDocumentStatsProto* put_document_stats = nullptr);
 
  private:
diff --git a/icing/index/index-processor_benchmark.cc b/icing/index/index-processor_benchmark.cc
index 8f5e319..b849bad 100644
--- a/icing/index/index-processor_benchmark.cc
+++ b/icing/index/index-processor_benchmark.cc
@@ -24,6 +24,7 @@
 #include "gmock/gmock.h"
 #include "third_party/absl/flags/flag.h"
 #include "icing/document-builder.h"
+#include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
 #include "icing/index/data-indexing-handler.h"
 #include "icing/index/index-processor.h"
@@ -37,14 +38,15 @@
 #include "icing/schema/schema-store.h"
 #include "icing/store/document-id.h"
 #include "icing/testing/common-matchers.h"
-#include "icing/testing/icu-data-file-helper.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/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
 #include "icing/util/clock.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "icing/util/logging.h"
 #include "icing/util/status-macros.h"
 #include "icing/util/tokenized-document.h"
@@ -169,14 +171,15 @@
       .ValueOrDie();
 }
 
-std::unique_ptr<SchemaStore> CreateSchemaStore(const Filesystem& filesystem,
-                                               const Clock* clock,
-                                               const std::string& base_dir) {
+std::unique_ptr<SchemaStore> CreateSchemaStore(
+    const Filesystem& filesystem, const Clock* clock,
+    const std::string& base_dir, const FeatureFlags& feature_flags) {
   std::string schema_store_dir = base_dir + "/schema_store_test";
   filesystem.CreateDirectoryRecursively(schema_store_dir.c_str());
 
   std::unique_ptr<SchemaStore> schema_store =
-      SchemaStore::Create(&filesystem, schema_store_dir, clock).ValueOrDie();
+      SchemaStore::Create(&filesystem, schema_store_dir, clock, &feature_flags)
+          .ValueOrDie();
 
   SchemaProto schema;
   CreateFakeTypeConfig(schema.add_types());
@@ -217,10 +220,11 @@
 void BM_IndexDocumentWithOneProperty(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   IcingFilesystem icing_filesystem;
   Filesystem filesystem;
   std::string base_dir = GetTestTempDir() + "/index_processor_benchmark";
@@ -244,7 +248,7 @@
   std::unique_ptr<Normalizer> normalizer = CreateNormalizer();
   Clock clock;
   std::unique_ptr<SchemaStore> schema_store =
-      CreateSchemaStore(filesystem, &clock, base_dir);
+      CreateSchemaStore(filesystem, &clock, base_dir, feature_flags);
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::vector<std::unique_ptr<DataIndexingHandler>> handlers,
@@ -259,10 +263,12 @@
                                 input_document)
           .ValueOrDie()));
 
+  DocumentId old_document_id = kInvalidDocumentId;
   DocumentId document_id = 0;
   for (auto _ : state) {
-    ICING_ASSERT_OK(
-        index_processor->IndexDocument(tokenized_document, document_id++));
+    ICING_ASSERT_OK(index_processor->IndexDocument(
+        tokenized_document, document_id, old_document_id));
+    old_document_id = document_id++;
   }
 
   index_processor.reset();
@@ -293,10 +299,11 @@
 void BM_IndexDocumentWithTenProperties(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   IcingFilesystem icing_filesystem;
   Filesystem filesystem;
   std::string base_dir = GetTestTempDir() + "/index_processor_benchmark";
@@ -320,7 +327,7 @@
   std::unique_ptr<Normalizer> normalizer = CreateNormalizer();
   Clock clock;
   std::unique_ptr<SchemaStore> schema_store =
-      CreateSchemaStore(filesystem, &clock, base_dir);
+      CreateSchemaStore(filesystem, &clock, base_dir, feature_flags);
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::vector<std::unique_ptr<DataIndexingHandler>> handlers,
@@ -336,10 +343,12 @@
                                 input_document)
           .ValueOrDie()));
 
+  DocumentId old_document_id = kInvalidDocumentId;
   DocumentId document_id = 0;
   for (auto _ : state) {
-    ICING_ASSERT_OK(
-        index_processor->IndexDocument(tokenized_document, document_id++));
+    ICING_ASSERT_OK(index_processor->IndexDocument(
+        tokenized_document, document_id, old_document_id));
+    old_document_id = document_id++;
   }
 
   index_processor.reset();
@@ -370,10 +379,11 @@
 void BM_IndexDocumentWithDiacriticLetters(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   IcingFilesystem icing_filesystem;
   Filesystem filesystem;
   std::string base_dir = GetTestTempDir() + "/index_processor_benchmark";
@@ -397,7 +407,7 @@
   std::unique_ptr<Normalizer> normalizer = CreateNormalizer();
   Clock clock;
   std::unique_ptr<SchemaStore> schema_store =
-      CreateSchemaStore(filesystem, &clock, base_dir);
+      CreateSchemaStore(filesystem, &clock, base_dir, feature_flags);
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::vector<std::unique_ptr<DataIndexingHandler>> handlers,
@@ -413,10 +423,12 @@
                                 input_document)
           .ValueOrDie()));
 
+  DocumentId old_document_id = kInvalidDocumentId;
   DocumentId document_id = 0;
   for (auto _ : state) {
-    ICING_ASSERT_OK(
-        index_processor->IndexDocument(tokenized_document, document_id++));
+    ICING_ASSERT_OK(index_processor->IndexDocument(
+        tokenized_document, document_id, old_document_id));
+    old_document_id = document_id++;
   }
 
   index_processor.reset();
@@ -447,10 +459,11 @@
 void BM_IndexDocumentWithHiragana(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   IcingFilesystem icing_filesystem;
   Filesystem filesystem;
   std::string base_dir = GetTestTempDir() + "/index_processor_benchmark";
@@ -474,7 +487,7 @@
   std::unique_ptr<Normalizer> normalizer = CreateNormalizer();
   Clock clock;
   std::unique_ptr<SchemaStore> schema_store =
-      CreateSchemaStore(filesystem, &clock, base_dir);
+      CreateSchemaStore(filesystem, &clock, base_dir, feature_flags);
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::vector<std::unique_ptr<DataIndexingHandler>> handlers,
@@ -489,10 +502,12 @@
                                 input_document)
           .ValueOrDie()));
 
+  DocumentId old_document_id = kInvalidDocumentId;
   DocumentId document_id = 0;
   for (auto _ : state) {
-    ICING_ASSERT_OK(
-        index_processor->IndexDocument(tokenized_document, document_id++));
+    ICING_ASSERT_OK(index_processor->IndexDocument(
+        tokenized_document, document_id, old_document_id));
+    old_document_id = document_id++;
   }
 
   index_processor.reset();
diff --git a/icing/index/index-processor_test.cc b/icing/index/index-processor_test.cc
index 2dce924..6edcd52 100644
--- a/icing/index/index-processor_test.cc
+++ b/icing/index/index-processor_test.cc
@@ -29,6 +29,7 @@
 #include "icing/absl_ports/str_cat.h"
 #include "icing/absl_ports/str_join.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/index/data-indexing-handler.h"
@@ -58,15 +59,16 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/random-string.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/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
 #include "icing/util/crc32.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "icing/util/tokenized-document.h"
 #include "unicode/uloc.h"
 
@@ -154,10 +156,11 @@
 class IndexProcessorTest : public Test {
  protected:
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
 
@@ -203,8 +206,8 @@
     ASSERT_TRUE(
         filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str()));
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     SchemaProto schema =
         SchemaBuilder()
             .AddType(
@@ -286,14 +289,14 @@
     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(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     doc_store_ = std::move(create_result.document_store);
 
     ICING_ASSERT_OK_AND_ASSIGN(
@@ -336,6 +339,7 @@
 
   std::unique_ptr<IcingMockFilesystem> mock_icing_filesystem_;
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   IcingFilesystem icing_filesystem_;
   FakeClock fake_clock_;
@@ -392,8 +396,10 @@
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 }
 
@@ -408,8 +414,10 @@
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 }
 
@@ -424,8 +432,10 @@
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -459,8 +469,10 @@
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   std::string coffeeRepeatedString = "coffee";
@@ -480,8 +492,10 @@
       tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId1),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId1,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId1));
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -540,8 +554,10 @@
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -567,8 +583,10 @@
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -603,9 +621,11 @@
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED,
-                       testing::HasSubstr("Hit buffer is full!")));
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED,
+               testing::HasSubstr("Hit buffer is full!")));
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 }
 
@@ -625,8 +645,10 @@
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 }
 
@@ -661,8 +683,10 @@
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   // "good" should have been indexed normally.
@@ -708,8 +732,10 @@
                                 document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(8));
 
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   // Query the lite index. "r" should have 5 matches.
@@ -750,8 +776,10 @@
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   document =
@@ -764,8 +792,10 @@
       tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId1),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId1,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId1));
 
   // Only document_id 1 should surface in a prefix query for "Rock"
@@ -790,8 +820,10 @@
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   document =
@@ -804,8 +836,10 @@
       tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId1),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId1,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId1));
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -833,8 +867,10 @@
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId1),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId1,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId1));
 
   ICING_ASSERT_OK_AND_ASSIGN(int64_t index_element_size,
@@ -855,8 +891,10 @@
       tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   // Verify that both index_ and integer_index_ are unchanged.
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId1));
   EXPECT_THAT(index_->GetElementsSize(), IsOkAndHolds(index_element_size));
@@ -865,8 +903,10 @@
               IsOkAndHolds(integer_index_crc));
 
   // As should indexing a document document_id == last_added_document_id.
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId1),
-              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId1,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   // Verify that both index_ and integer_index_ are unchanged.
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId1));
   EXPECT_THAT(index_->GetElementsSize(), IsOkAndHolds(index_element_size));
@@ -909,8 +949,10 @@
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor.IndexDocument(tokenized_document, kDocumentId1),
-              IsOk());
+  EXPECT_THAT(
+      index_processor.IndexDocument(tokenized_document, kDocumentId1,
+                                    /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId1));
 
   ICING_ASSERT_OK_AND_ASSIGN(int64_t index_element_size,
@@ -932,8 +974,10 @@
       tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor.IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor.IndexDocument(tokenized_document, kDocumentId0,
+                                    /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   // Verify that both index_ and integer_index_ are unchanged.
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId1));
   EXPECT_THAT(index_->GetElementsSize(), IsOkAndHolds(index_element_size));
@@ -942,8 +986,10 @@
               IsOkAndHolds(integer_index_crc));
 
   // As should indexing a document document_id == last_added_document_id.
-  EXPECT_THAT(index_processor.IndexDocument(tokenized_document, kDocumentId1),
-              IsOk());
+  EXPECT_THAT(
+      index_processor.IndexDocument(tokenized_document, kDocumentId1,
+                                    /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   // Verify that both index_ and integer_index_ are unchanged.
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId1));
   EXPECT_THAT(index_->GetElementsSize(), IsOkAndHolds(index_element_size));
@@ -970,8 +1016,10 @@
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1004,8 +1052,10 @@
       TokenizedDocument tokenized_document,
       TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
                                 document_one));
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 }
 
@@ -1050,12 +1100,18 @@
   // empties the LiteIndex.
   constexpr int kNumDocsLiteIndexExhaustion = 3373;
   for (; doc_id < kNumDocsLiteIndexExhaustion; ++doc_id) {
-    EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, doc_id),
-                IsOk());
+    EXPECT_THAT(
+        index_processor_->IndexDocument(
+            tokenized_document, doc_id,
+            /*old_document_id=*/doc_id == 0 ? kInvalidDocumentId : doc_id - 1),
+        IsOk());
     EXPECT_THAT(index_->last_added_document_id(), Eq(doc_id));
   }
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, doc_id),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(
+          tokenized_document, doc_id,
+          /*old_document_id=*/doc_id == 0 ? kInvalidDocumentId : doc_id - 1),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(doc_id));
 }
 
@@ -1112,19 +1168,23 @@
   // 3. Index one document. This should fit in the LiteIndex without requiring a
   // merge.
   DocumentId doc_id = 0;
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, doc_id),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, doc_id,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(doc_id));
 
   // 4. Add one more document to trigger a merge, which should fail and result
   // in a Reset.
   ++doc_id;
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, doc_id),
+  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, doc_id,
+                                              /*old_document_id=*/doc_id - 1),
               StatusIs(libtextclassifier3::StatusCode::DATA_LOSS));
   EXPECT_THAT(index_->last_added_document_id(), Eq(kInvalidDocumentId));
 
   // 5. Indexing a new document should succeed.
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, doc_id),
+  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, doc_id,
+                                              /*old_document_id=*/doc_id - 1),
               IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(doc_id));
 }
@@ -1143,8 +1203,10 @@
                                 document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(1));
 
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1175,8 +1237,10 @@
                                 document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(1));
 
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   // We expect to match the document we indexed as "Hello, w" is a prefix
@@ -1209,8 +1273,10 @@
                                 document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(1));
 
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1240,8 +1306,10 @@
                                 document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(7));
 
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   std::unordered_map<SectionId, Hit::TermFrequency> expected_map{
@@ -1289,8 +1357,10 @@
                                 document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(7));
 
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   std::unordered_map<SectionId, Hit::TermFrequency> expected_map{
@@ -1319,8 +1389,10 @@
                                 document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(7));
 
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   // "alexsav@" only matches "alexsav@google.com"
@@ -1370,8 +1442,10 @@
                                 document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(7));
 
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   std::unordered_map<SectionId, Hit::TermFrequency> expect_map{{}};
@@ -1401,8 +1475,10 @@
                                 document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(7));
 
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1459,8 +1535,10 @@
                                 document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(8));
 
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1501,8 +1579,10 @@
                                 document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(7));
 
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   // "goo" is a prefix of "google" and "google.com"
@@ -1553,8 +1633,10 @@
                                 document));
   EXPECT_THAT(tokenized_document.num_string_tokens(), Eq(8));
 
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
   EXPECT_THAT(index_->last_added_document_id(), Eq(kDocumentId0));
 
   // no token starts with "gle", so we should have no hits
@@ -1606,8 +1688,10 @@
   // Expected to have 1 integer section.
   EXPECT_THAT(tokenized_document.integer_sections(), SizeIs(1));
 
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<DocHitInfoIterator> itr,
@@ -1636,8 +1720,10 @@
   // Expected to have 1 integer section.
   EXPECT_THAT(tokenized_document.integer_sections(), SizeIs(1));
 
-  EXPECT_THAT(index_processor_->IndexDocument(tokenized_document, kDocumentId0),
-              IsOk());
+  EXPECT_THAT(
+      index_processor_->IndexDocument(tokenized_document, kDocumentId0,
+                                      /*old_document_id=*/kInvalidDocumentId),
+      IsOk());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<DocHitInfoIterator> itr,
diff --git a/icing/index/index.cc b/icing/index/index.cc
index 54031fd..a03f07a 100644
--- a/icing/index/index.cc
+++ b/icing/index/index.cc
@@ -19,6 +19,7 @@
 #include <cstdint>
 #include <memory>
 #include <string>
+#include <string_view>
 #include <utility>
 #include <vector>
 
@@ -308,7 +309,8 @@
                                new_last_added_document_id);
 }
 
-libtextclassifier3::Status Index::Editor::BufferTerm(const char* term) {
+libtextclassifier3::Status Index::Editor::BufferTerm(
+    std::string_view term, TermMatchType::Code match_type) {
   // Step 1: See if this term is already in the lexicon
   uint32_t tvi;
   auto tvi_or = lite_index_->GetTermId(term);
@@ -316,10 +318,19 @@
   // Step 2: Update the lexicon, either add the term or update its properties
   if (tvi_or.ok()) {
     tvi = tvi_or.ValueOrDie();
-    if (seen_tokens_.find(tvi) != seen_tokens_.end()) {
-      ICING_VLOG(1) << "Updating term frequency for term " << term;
-      if (seen_tokens_[tvi] != Hit::kMaxTermFrequency) {
-        ++seen_tokens_[tvi];
+    auto itr = seen_tokens_.find(tvi);
+    if (itr != seen_tokens_.end()) {
+      HitDetails& hit_details = itr->second;
+      if (hit_details.match_type == TermMatchType::EXACT_ONLY &&
+          match_type == TermMatchType::PREFIX) {
+        // If the same term is added multiple times, but with different match
+        // types, we will always update the match type to PREFIX.
+        ICING_VLOG(1) << "Updating term match type for term " << term;
+        hit_details.match_type = TermMatchType::PREFIX;
+      }
+      if (hit_details.term_frequency != Hit::kMaxTermFrequency) {
+        ICING_VLOG(1) << "Updating term frequency for term " << term;
+        ++hit_details.term_frequency;
       }
       return libtextclassifier3::Status::OK;
     }
@@ -327,22 +338,23 @@
                   << " is already present in lexicon. Updating.";
     // Already in the lexicon. Just update the properties.
     ICING_RETURN_IF_ERROR(lite_index_->UpdateTermProperties(
-        tvi, term_match_type_ == TermMatchType::PREFIX, namespace_id_));
+        tvi, match_type == TermMatchType::PREFIX, namespace_id_));
   } else {
     ICING_VLOG(1) << "Term " << term << " is not in lexicon. Inserting.";
     // Haven't seen this term before. Add it to the lexicon.
     ICING_ASSIGN_OR_RETURN(
-        tvi, lite_index_->InsertTerm(term, term_match_type_, namespace_id_));
+        tvi, lite_index_->InsertTerm(term, match_type, namespace_id_));
   }
   // Token seen for the first time in the current document.
-  seen_tokens_[tvi] = 1;
+  seen_tokens_[tvi] = {match_type, /*term_frequency=*/1};
   return libtextclassifier3::Status::OK;
 }
 
 libtextclassifier3::Status Index::Editor::IndexAllBufferedTerms() {
   for (auto itr = seen_tokens_.begin(); itr != seen_tokens_.end(); itr++) {
-    Hit hit(section_id_, document_id_, /*term_frequency=*/itr->second,
-            /*is_in_prefix_section=*/term_match_type_ == TermMatchType::PREFIX,
+    Hit hit(section_id_, document_id_, itr->second.term_frequency,
+            /*is_in_prefix_section=*/itr->second.match_type ==
+                TermMatchType::PREFIX,
             /*is_prefix_hit=*/false, /*is_stemmed_hit=*/false);
     ICING_ASSIGN_OR_RETURN(
         uint32_t term_id, term_id_codec_->EncodeTvi(itr->first, TviType::LITE));
diff --git a/icing/index/index.h b/icing/index/index.h
index 0d436e2..fe3415c 100644
--- a/icing/index/index.h
+++ b/icing/index/index.h
@@ -18,6 +18,7 @@
 #include <cstdint>
 #include <memory>
 #include <string>
+#include <string_view>
 #include <unordered_map>
 #include <utility>
 #include <vector>
@@ -261,16 +262,16 @@
     // TODO(b/141180665): Add nullptr checks for the raw pointers
     Editor(const TermIdCodec* term_id_codec, LiteIndex* lite_index,
            DocumentId document_id, SectionId section_id,
-           TermMatchType::Code term_match_type, NamespaceId namespace_id)
+           NamespaceId namespace_id)
         : term_id_codec_(term_id_codec),
           lite_index_(lite_index),
           document_id_(document_id),
-          term_match_type_(term_match_type),
           namespace_id_(namespace_id),
           section_id_(section_id) {}
 
     // Buffer the term in seen_tokens_.
-    libtextclassifier3::Status BufferTerm(const char* term);
+    libtextclassifier3::Status BufferTerm(std::string_view term,
+                                          TermMatchType::Code match_type);
     // Index all the terms stored in seen_tokens_.
     libtextclassifier3::Status IndexAllBufferedTerms();
 
@@ -278,18 +279,21 @@
     // The Editor is able to store previously seen terms as TermIds. This is
     // is more efficient than a client doing this externally because TermIds are
     // not exposed to clients.
-    std::unordered_map<uint32_t, Hit::TermFrequency> seen_tokens_;
+    struct HitDetails {
+      TermMatchType::Code match_type;
+      Hit::TermFrequency term_frequency;
+    };
+    std::unordered_map<uint32_t, HitDetails> seen_tokens_;
     const TermIdCodec* term_id_codec_;
     LiteIndex* lite_index_;
     DocumentId document_id_;
-    TermMatchType::Code term_match_type_;
     NamespaceId namespace_id_;
     SectionId section_id_;
   };
   Editor Edit(DocumentId document_id, SectionId section_id,
-              TermMatchType::Code term_match_type, NamespaceId namespace_id) {
+              NamespaceId namespace_id) {
     return Editor(term_id_codec_.get(), lite_index_.get(), document_id,
-                  section_id, term_match_type, namespace_id);
+                  section_id, namespace_id);
   }
 
   bool WantsMerge() const { return lite_index_->WantsMerge(); }
diff --git a/icing/index/index_test.cc b/icing/index/index_test.cc
index db94e78..7f73590 100644
--- a/icing/index/index_test.cc
+++ b/icing/index/index_test.cc
@@ -208,11 +208,11 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       index_, Index::Create(options, &filesystem_, &icing_filesystem_));
 
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
-  ASSERT_THAT(edit.BufferTerm("bar"), IsOk());
-  ASSERT_THAT(edit.BufferTerm("baz"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  ASSERT_THAT(edit.BufferTerm("bar", TermMatchType::EXACT_ONLY), IsOk());
+  ASSERT_THAT(edit.BufferTerm("baz", TermMatchType::EXACT_ONLY), IsOk());
   ASSERT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   // Persist and recreate the index with lite_index_sort_at_indexing=true
@@ -230,9 +230,9 @@
 }
 
 TEST_F(IndexTest, AdvancePastEnd) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -257,9 +257,9 @@
 }
 
 TEST_F(IndexTest, AdvancePastEndAfterMerge) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK(index_->Merge());
@@ -286,15 +286,15 @@
 }
 
 TEST_F(IndexTest, IteratorGetCallStats_mainIndexOnly) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("bar"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("bar", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId1, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId1, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   // Merge the index.
@@ -348,15 +348,15 @@
 }
 
 TEST_F(IndexTest, IteratorGetCallStats_liteIndexOnly) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("bar"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("bar", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId1, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId1, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -407,29 +407,29 @@
 }
 
 TEST_F(IndexTest, IteratorGetCallStats) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("bar"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("bar", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId1, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId1, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   // Merge the index. 2 hits for "foo" will be merged into the main index.
   ICING_ASSERT_OK(index_->Merge());
 
   // Insert 2 more hits for "foo". It will be in the lite index.
-  edit = index_->Edit(kDocumentId2, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId2, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId3, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId3, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -508,9 +508,9 @@
 }
 
 TEST_F(IndexTest, SingleHitSingleTermIndex) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -524,9 +524,9 @@
 }
 
 TEST_F(IndexTest, SingleHitSingleTermIndexAfterMerge) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK(index_->Merge());
@@ -542,9 +542,9 @@
 }
 
 TEST_F(IndexTest, SingleHitSingleTermIndexAfterOptimize) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId2, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId2, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   index_->set_last_added_document_id(kDocumentId2);
 
@@ -580,9 +580,9 @@
 }
 
 TEST_F(IndexTest, SingleHitSingleTermIndexAfterMergeAndOptimize) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId2, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId2, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   index_->set_last_added_document_id(kDocumentId2);
 
@@ -620,10 +620,10 @@
 }
 
 TEST_F(IndexTest, SingleHitMultiTermIndex) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("bar"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("bar", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -637,10 +637,10 @@
 }
 
 TEST_F(IndexTest, SingleHitMultiTermIndexAfterMerge) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("bar"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("bar", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK(index_->Merge());
@@ -656,19 +656,19 @@
 }
 
 TEST_F(IndexTest, MultiHitMultiTermIndexAfterOptimize) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId1, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId1, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("bar"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("bar", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId2, kSectionId3, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId2, kSectionId3,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   index_->set_last_added_document_id(kDocumentId2);
 
@@ -721,19 +721,19 @@
 }
 
 TEST_F(IndexTest, MultiHitMultiTermIndexAfterMergeAndOptimize) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId1, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId1, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("bar"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("bar", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId2, kSectionId3, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId2, kSectionId3,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   index_->set_last_added_document_id(kDocumentId2);
 
@@ -788,10 +788,10 @@
 }
 
 TEST_F(IndexTest, NoHitMultiTermIndex) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("bar"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("bar", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -804,10 +804,10 @@
 }
 
 TEST_F(IndexTest, NoHitMultiTermIndexAfterMerge) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("bar"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("bar", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK(index_->Merge());
@@ -822,19 +822,19 @@
 }
 
 TEST_F(IndexTest, MultiHitMultiTermIndex) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId1, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId1, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("bar"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("bar", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId2, kSectionId3, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId2, kSectionId3,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -850,19 +850,19 @@
 }
 
 TEST_F(IndexTest, MultiHitMultiTermIndexAfterMerge) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId1, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId1, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("bar"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("bar", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId2, kSectionId3, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId2, kSectionId3,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK(index_->Merge());
@@ -880,14 +880,14 @@
 }
 
 TEST_F(IndexTest, MultiHitSectionRestrict) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId1, kSectionId3, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId1, kSectionId3,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   SectionIdMask desired_section = 1U << kSectionId2;
@@ -902,14 +902,14 @@
 }
 
 TEST_F(IndexTest, MultiHitSectionRestrictAfterMerge) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId1, kSectionId3, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId1, kSectionId3,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK(index_->Merge());
@@ -928,12 +928,12 @@
 TEST_F(IndexTest, SingleHitDedupeIndex) {
   ICING_ASSERT_OK_AND_ASSIGN(int64_t size, index_->GetElementsSize());
   EXPECT_THAT(size, Eq(0));
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   ICING_ASSERT_OK_AND_ASSIGN(size, index_->GetElementsSize());
   EXPECT_THAT(size, Gt(0));
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   ICING_ASSERT_OK_AND_ASSIGN(int64_t new_size, index_->GetElementsSize());
   EXPECT_THAT(new_size, Eq(size));
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
@@ -950,8 +950,8 @@
 
 TEST_F(IndexTest, PrefixHit) {
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("fool"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("fool", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -966,8 +966,8 @@
 
 TEST_F(IndexTest, PrefixHitAfterMerge) {
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("fool"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("fool", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK(index_->Merge());
@@ -984,13 +984,13 @@
 
 TEST_F(IndexTest, MultiPrefixHit) {
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("fool"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("fool", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId1, kSectionId3, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId1, kSectionId3,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1007,13 +1007,13 @@
 
 TEST_F(IndexTest, MultiPrefixHitAfterMerge) {
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("fool"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("fool", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId1, kSectionId3, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId1, kSectionId3,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK(index_->Merge());
@@ -1031,14 +1031,14 @@
 }
 
 TEST_F(IndexTest, NoExactHitInPrefixQuery) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("fool"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId1, kSectionId3, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId1, kSectionId3,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1052,14 +1052,14 @@
 }
 
 TEST_F(IndexTest, NoExactHitInPrefixQueryAfterMerge) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("fool"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId1, kSectionId3, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId1, kSectionId3,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK(index_->Merge());
@@ -1076,9 +1076,9 @@
 
 TEST_F(IndexTest, PrefixHitDedupe) {
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
-  ASSERT_THAT(edit.BufferTerm("fool"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
+  ASSERT_THAT(edit.BufferTerm("fool", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1093,9 +1093,9 @@
 
 TEST_F(IndexTest, PrefixHitDedupeAfterMerge) {
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
-  ASSERT_THAT(edit.BufferTerm("fool"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
+  ASSERT_THAT(edit.BufferTerm("fool", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK(index_->Merge());
@@ -1174,9 +1174,9 @@
 
 TEST_F(IndexTest, NonAsciiTerms) {
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("こんにちは"), IsOk());
-  ASSERT_THAT(edit.BufferTerm("あなた"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("こんにちは", TermMatchType::PREFIX), IsOk());
+  ASSERT_THAT(edit.BufferTerm("あなた", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1199,9 +1199,9 @@
 
 TEST_F(IndexTest, NonAsciiTermsAfterMerge) {
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("こんにちは"), IsOk());
-  ASSERT_THAT(edit.BufferTerm("あなた"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("こんにちは", TermMatchType::PREFIX), IsOk());
+  ASSERT_THAT(edit.BufferTerm("あなた", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK(index_->Merge());
@@ -1246,11 +1246,11 @@
   std::uniform_int_distribution<size_t> uniform(0u, query_terms.size() - 1);
   while (status.ok()) {
     for (int i = 0; i < 100; ++i) {
-      Index::Editor edit =
-          index_->Edit(document_id, kSectionId2, TermMatchType::PREFIX,
-                       /*namespace_id=*/0);
+      Index::Editor edit = index_->Edit(document_id, kSectionId2,
+                                        /*namespace_id=*/0);
       size_t idx = uniform(random);
-      status = edit.BufferTerm(query_terms.at(idx).c_str());
+      status =
+          edit.BufferTerm(query_terms.at(idx).c_str(), TermMatchType::PREFIX);
       if (!status.ok()) {
         break;
       }
@@ -1263,15 +1263,14 @@
   }
 
   // Adding more hits should fail.
-  Index::Editor edit =
-      index_->Edit(document_id + 1, kSectionId2, TermMatchType::PREFIX,
-                   /*namespace_id=*/0);
+  Index::Editor edit = index_->Edit(document_id + 1, kSectionId2,
+                                    /*namespace_id=*/0);
   std::string term = prefix + "foo";
-  EXPECT_THAT(edit.BufferTerm(term.c_str()), IsOk());
+  EXPECT_THAT(edit.BufferTerm(term.c_str(), TermMatchType::PREFIX), IsOk());
   term = prefix + "bar";
-  EXPECT_THAT(edit.BufferTerm(term.c_str()), IsOk());
+  EXPECT_THAT(edit.BufferTerm(term.c_str(), TermMatchType::PREFIX), IsOk());
   term = prefix + "baz";
-  EXPECT_THAT(edit.BufferTerm(term.c_str()), IsOk());
+  EXPECT_THAT(edit.BufferTerm(term.c_str(), TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
 
@@ -1316,11 +1315,11 @@
   std::uniform_int_distribution<size_t> uniform(0u, query_terms.size() - 1);
   while (status.ok()) {
     for (int i = 0; i < 100; ++i) {
-      Index::Editor edit =
-          index_->Edit(document_id, kSectionId2, TermMatchType::PREFIX,
-                       /*namespace_id=*/0);
+      Index::Editor edit = index_->Edit(document_id, kSectionId2,
+                                        /*namespace_id=*/0);
       size_t idx = uniform(random);
-      status = edit.BufferTerm(query_terms.at(idx).c_str());
+      status =
+          edit.BufferTerm(query_terms.at(idx).c_str(), TermMatchType::PREFIX);
       if (!status.ok()) {
         break;
       }
@@ -1335,15 +1334,14 @@
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
 
   // Adding more hits should fail.
-  Index::Editor edit =
-      index_->Edit(document_id + 1, kSectionId2, TermMatchType::PREFIX,
-                   /*namespace_id=*/0);
+  Index::Editor edit = index_->Edit(document_id + 1, kSectionId2,
+                                    /*namespace_id=*/0);
   std::string term = prefix + "foo";
-  EXPECT_THAT(edit.BufferTerm(term.c_str()), IsOk());
+  EXPECT_THAT(edit.BufferTerm(term.c_str(), TermMatchType::PREFIX), IsOk());
   term = prefix + "bar";
-  EXPECT_THAT(edit.BufferTerm(term.c_str()), IsOk());
+  EXPECT_THAT(edit.BufferTerm(term.c_str(), TermMatchType::PREFIX), IsOk());
   term = prefix + "baz";
-  EXPECT_THAT(edit.BufferTerm(term.c_str()), IsOk());
+  EXPECT_THAT(edit.BufferTerm(term.c_str(), TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1356,13 +1354,13 @@
 
   // After merging with the main index. Adding more hits should succeed now.
   ICING_ASSERT_OK(index_->Merge());
-  edit = index_->Edit(document_id + 1, kSectionId2, TermMatchType::PREFIX, 0);
+  edit = index_->Edit(document_id + 1, kSectionId2, 0);
   prefix + "foo";
-  EXPECT_THAT(edit.BufferTerm(term.c_str()), IsOk());
+  EXPECT_THAT(edit.BufferTerm(term.c_str(), TermMatchType::PREFIX), IsOk());
   term = prefix + "bar";
-  EXPECT_THAT(edit.BufferTerm(term.c_str()), IsOk());
+  EXPECT_THAT(edit.BufferTerm(term.c_str(), TermMatchType::PREFIX), IsOk());
   term = prefix + "baz";
-  EXPECT_THAT(edit.BufferTerm(term.c_str()), IsOk());
+  EXPECT_THAT(edit.BufferTerm(term.c_str(), TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<DocHitInfoIterator> itr,
@@ -1420,8 +1418,9 @@
       term_match_type = TermMatchType::EXACT_ONLY;
     }
     Index::Editor edit = index_->Edit(/*document_id=*/i, section_id,
-                                      term_match_type, /*namespace_id=*/0);
-    ICING_ASSERT_OK(edit.BufferTerm(query_terms.at(i).c_str()));
+                                      /*namespace_id=*/0);
+    ICING_ASSERT_OK(
+        edit.BufferTerm(query_terms.at(i).c_str(), term_match_type));
     ICING_ASSERT_OK(edit.IndexAllBufferedTerms());
   }
 
@@ -1478,8 +1477,8 @@
       term = prefix + RandomString("abcd", 5, &random);
     }
     Index::Editor edit = index_->Edit(/*document_id=*/i, section_id,
-                                      term_match_type, /*namespace_id=*/0);
-    ICING_ASSERT_OK(edit.BufferTerm(term.c_str()));
+                                      /*namespace_id=*/0);
+    ICING_ASSERT_OK(edit.BufferTerm(term.c_str(), term_match_type));
     ICING_ASSERT_OK(edit.IndexAllBufferedTerms());
     ++lite_index_size;
     index_->set_last_added_document_id(i);
@@ -1542,8 +1541,9 @@
       term_match_type = TermMatchType::EXACT_ONLY;
     }
     Index::Editor edit = index_->Edit(/*document_id=*/i, section_id,
-                                      term_match_type, /*namespace_id=*/0);
-    ICING_ASSERT_OK(edit.BufferTerm(query_terms.at(i).c_str()));
+                                      /*namespace_id=*/0);
+    ICING_ASSERT_OK(
+        edit.BufferTerm(query_terms.at(i).c_str(), term_match_type));
     ICING_ASSERT_OK(edit.IndexAllBufferedTerms());
     index_->set_last_added_document_id(i);
   }
@@ -1641,9 +1641,9 @@
 TEST_F(IndexTest, IndexCreateCorruptionFailure) {
   // Add some content to the index
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
-  ASSERT_THAT(edit.BufferTerm("bar"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
+  ASSERT_THAT(edit.BufferTerm("bar", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   // Close the index.
@@ -1674,9 +1674,9 @@
 TEST_F(IndexTest, UpdateChecksum) {
   // Add some content to the index
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
-  ASSERT_THAT(edit.BufferTerm("bar"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
+  ASSERT_THAT(edit.BufferTerm("bar", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   Crc32 lite_only_crc = index_->GetChecksum();
   EXPECT_THAT(index_->UpdateChecksum(), Eq(lite_only_crc));
@@ -1690,10 +1690,10 @@
   EXPECT_THAT(index_->GetChecksum(), Eq(main_only_crc));
 
   // Add some more content to the lite index
-  edit = index_->Edit(kDocumentId1, kSectionId2, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId1, kSectionId2,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("baz"), IsOk());
-  ASSERT_THAT(edit.BufferTerm("bat"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("baz", TermMatchType::PREFIX), IsOk());
+  ASSERT_THAT(edit.BufferTerm("bat", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   Crc32 both_crc = index_->GetChecksum();
   EXPECT_THAT(both_crc, Not(Eq(lite_only_crc)));
@@ -1705,9 +1705,9 @@
 TEST_F(IndexTest, IndexPersistence) {
   // Add some content to the index
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
-  ASSERT_THAT(edit.BufferTerm("bar"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
+  ASSERT_THAT(edit.BufferTerm("bar", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   EXPECT_THAT(index_->PersistToDisk(), IsOk());
 
@@ -1735,9 +1735,9 @@
 TEST_F(IndexTest, IndexPersistenceAfterMerge) {
   // Add some content to the index
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
-  ASSERT_THAT(edit.BufferTerm("bar"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
+  ASSERT_THAT(edit.BufferTerm("bar", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   ICING_ASSERT_OK(index_->Merge());
   EXPECT_THAT(index_->PersistToDisk(), IsOk());
@@ -1773,9 +1773,9 @@
 
 TEST_F(IndexTest, FindTermByPrefixShouldReturnEmpty) {
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
+                                    /*namespace_id=*/0);
   AlwaysTrueSuggestionResultCheckerImpl impl;
-  EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   EXPECT_THAT(
@@ -1808,11 +1808,11 @@
 }
 
 TEST_F(IndexTest, FindTermByPrefixShouldReturnCorrectResult) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
   AlwaysTrueSuggestionResultCheckerImpl impl;
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("bar"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("bar", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   // "b" should only match "bar" but not "foo".
@@ -1835,12 +1835,12 @@
 }
 
 TEST_F(IndexTest, FindTermByPrefixShouldRespectNumToReturn) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
   AlwaysTrueSuggestionResultCheckerImpl impl;
-  EXPECT_THAT(edit.BufferTerm("fo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   // We have 3 results but only 2 should be returned.
@@ -1863,23 +1863,20 @@
 }
 
 TEST_F(IndexTest, FindTermByPrefixShouldReturnTermsInAllNamespaces) {
-  Index::Editor edit1 =
-      index_->Edit(kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/0);
+  Index::Editor edit1 = index_->Edit(kDocumentId0, kSectionId2,
+                                     /*namespace_id=*/0);
   AlwaysTrueSuggestionResultCheckerImpl impl;
-  EXPECT_THAT(edit1.BufferTerm("fo"), IsOk());
+  EXPECT_THAT(edit1.BufferTerm("fo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit1.IndexAllBufferedTerms(), IsOk());
 
-  Index::Editor edit2 =
-      index_->Edit(kDocumentId1, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/1);
-  EXPECT_THAT(edit2.BufferTerm("foo"), IsOk());
+  Index::Editor edit2 = index_->Edit(kDocumentId1, kSectionId2,
+                                     /*namespace_id=*/1);
+  EXPECT_THAT(edit2.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit2.IndexAllBufferedTerms(), IsOk());
 
-  Index::Editor edit3 =
-      index_->Edit(kDocumentId2, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/2);
-  EXPECT_THAT(edit3.BufferTerm("fool"), IsOk());
+  Index::Editor edit3 = index_->Edit(kDocumentId2, kSectionId2,
+                                     /*namespace_id=*/2);
+  EXPECT_THAT(edit3.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit3.IndexAllBufferedTerms(), IsOk());
 
   // Should return "fo", "foo" and "fool" across all namespaces.
@@ -1906,18 +1903,16 @@
 }
 
 TEST_F(IndexTest, FindTermByPrefixShouldReturnCorrectHitCount) {
-  Index::Editor edit1 =
-      index_->Edit(kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/0);
+  Index::Editor edit1 = index_->Edit(kDocumentId0, kSectionId2,
+                                     /*namespace_id=*/0);
   AlwaysTrueSuggestionResultCheckerImpl impl;
-  EXPECT_THAT(edit1.BufferTerm("foo"), IsOk());
-  EXPECT_THAT(edit1.BufferTerm("fool"), IsOk());
+  EXPECT_THAT(edit1.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit1.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit1.IndexAllBufferedTerms(), IsOk());
 
-  Index::Editor edit2 =
-      index_->Edit(kDocumentId1, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/0);
-  EXPECT_THAT(edit2.BufferTerm("fool"), IsOk());
+  Index::Editor edit2 = index_->Edit(kDocumentId1, kSectionId2,
+                                     /*namespace_id=*/0);
+  EXPECT_THAT(edit2.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit2.IndexAllBufferedTerms(), IsOk());
 
   // 'foo' has 1 hit, 'fool' has 2 hits.
@@ -1944,9 +1939,9 @@
   AlwaysTrueSuggestionResultCheckerImpl impl;
   // Create multiple hit batches.
   for (int i = 0; i < 4000; i++) {
-    Index::Editor edit = index_->Edit(i, kSectionId2, TermMatchType::EXACT_ONLY,
+    Index::Editor edit = index_->Edit(i, kSectionId2,
                                       /*namespace_id=*/0);
-    EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+    EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
     EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   }
 
@@ -1970,56 +1965,53 @@
 TEST_F(IndexTest, FindTermByPrefixShouldReturnInOrder) {
   // Push 6 term-six, 5 term-five, 4 term-four, 3 term-three, 2 term-two and one
   // term-one into lite index.
-  Index::Editor edit1 =
-      index_->Edit(kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/0);
+  Index::Editor edit1 = index_->Edit(kDocumentId0, kSectionId2,
+                                     /*namespace_id=*/0);
   AlwaysTrueSuggestionResultCheckerImpl impl;
-  EXPECT_THAT(edit1.BufferTerm("term-one"), IsOk());
-  EXPECT_THAT(edit1.BufferTerm("term-two"), IsOk());
-  EXPECT_THAT(edit1.BufferTerm("term-three"), IsOk());
-  EXPECT_THAT(edit1.BufferTerm("term-four"), IsOk());
-  EXPECT_THAT(edit1.BufferTerm("term-five"), IsOk());
-  EXPECT_THAT(edit1.BufferTerm("term-six"), IsOk());
+  EXPECT_THAT(edit1.BufferTerm("term-one", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit1.BufferTerm("term-two", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit1.BufferTerm("term-three", TermMatchType::EXACT_ONLY),
+              IsOk());
+  EXPECT_THAT(edit1.BufferTerm("term-four", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit1.BufferTerm("term-five", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit1.BufferTerm("term-six", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit1.IndexAllBufferedTerms(), IsOk());
 
-  Index::Editor edit2 =
-      index_->Edit(kDocumentId2, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/0);
-  EXPECT_THAT(edit2.BufferTerm("term-two"), IsOk());
-  EXPECT_THAT(edit2.BufferTerm("term-three"), IsOk());
-  EXPECT_THAT(edit2.BufferTerm("term-four"), IsOk());
-  EXPECT_THAT(edit2.BufferTerm("term-five"), IsOk());
-  EXPECT_THAT(edit2.BufferTerm("term-six"), IsOk());
+  Index::Editor edit2 = index_->Edit(kDocumentId2, kSectionId2,
+                                     /*namespace_id=*/0);
+  EXPECT_THAT(edit2.BufferTerm("term-two", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit2.BufferTerm("term-three", TermMatchType::EXACT_ONLY),
+              IsOk());
+  EXPECT_THAT(edit2.BufferTerm("term-four", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit2.BufferTerm("term-five", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit2.BufferTerm("term-six", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit2.IndexAllBufferedTerms(), IsOk());
 
-  Index::Editor edit3 =
-      index_->Edit(kDocumentId3, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/0);
-  EXPECT_THAT(edit3.BufferTerm("term-three"), IsOk());
-  EXPECT_THAT(edit3.BufferTerm("term-four"), IsOk());
-  EXPECT_THAT(edit3.BufferTerm("term-five"), IsOk());
-  EXPECT_THAT(edit3.BufferTerm("term-six"), IsOk());
+  Index::Editor edit3 = index_->Edit(kDocumentId3, kSectionId2,
+                                     /*namespace_id=*/0);
+  EXPECT_THAT(edit3.BufferTerm("term-three", TermMatchType::EXACT_ONLY),
+              IsOk());
+  EXPECT_THAT(edit3.BufferTerm("term-four", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit3.BufferTerm("term-five", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit3.BufferTerm("term-six", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit3.IndexAllBufferedTerms(), IsOk());
 
-  Index::Editor edit4 =
-      index_->Edit(kDocumentId4, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/0);
-  EXPECT_THAT(edit4.BufferTerm("term-four"), IsOk());
-  EXPECT_THAT(edit4.BufferTerm("term-five"), IsOk());
-  EXPECT_THAT(edit4.BufferTerm("term-six"), IsOk());
+  Index::Editor edit4 = index_->Edit(kDocumentId4, kSectionId2,
+                                     /*namespace_id=*/0);
+  EXPECT_THAT(edit4.BufferTerm("term-four", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit4.BufferTerm("term-five", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit4.BufferTerm("term-six", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit4.IndexAllBufferedTerms(), IsOk());
 
-  Index::Editor edit5 =
-      index_->Edit(kDocumentId5, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/0);
-  EXPECT_THAT(edit5.BufferTerm("term-five"), IsOk());
-  EXPECT_THAT(edit5.BufferTerm("term-six"), IsOk());
+  Index::Editor edit5 = index_->Edit(kDocumentId5, kSectionId2,
+                                     /*namespace_id=*/0);
+  EXPECT_THAT(edit5.BufferTerm("term-five", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit5.BufferTerm("term-six", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit5.IndexAllBufferedTerms(), IsOk());
 
-  Index::Editor edit6 =
-      index_->Edit(kDocumentId6, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/0);
-  EXPECT_THAT(edit6.BufferTerm("term-six"), IsOk());
+  Index::Editor edit6 = index_->Edit(kDocumentId6, kSectionId2,
+                                     /*namespace_id=*/0);
+  EXPECT_THAT(edit6.BufferTerm("term-six", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit6.IndexAllBufferedTerms(), IsOk());
 
   // verify the order in lite index is correct.
@@ -2051,20 +2043,20 @@
 
   // keep push terms to the lite index. We will add 2 document to term-five,
   // term-three and term-one. The output order should be 5-6-3-4-1-2.
-  Index::Editor edit7 =
-      index_->Edit(kDocumentId7, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/0);
-  EXPECT_THAT(edit7.BufferTerm("term-one"), IsOk());
-  EXPECT_THAT(edit7.BufferTerm("term-three"), IsOk());
-  EXPECT_THAT(edit7.BufferTerm("term-five"), IsOk());
+  Index::Editor edit7 = index_->Edit(kDocumentId7, kSectionId2,
+                                     /*namespace_id=*/0);
+  EXPECT_THAT(edit7.BufferTerm("term-one", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit7.BufferTerm("term-three", TermMatchType::EXACT_ONLY),
+              IsOk());
+  EXPECT_THAT(edit7.BufferTerm("term-five", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit7.IndexAllBufferedTerms(), IsOk());
 
-  Index::Editor edit8 =
-      index_->Edit(kDocumentId8, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/0);
-  EXPECT_THAT(edit8.BufferTerm("term-one"), IsOk());
-  EXPECT_THAT(edit8.BufferTerm("term-three"), IsOk());
-  EXPECT_THAT(edit8.BufferTerm("term-five"), IsOk());
+  Index::Editor edit8 = index_->Edit(kDocumentId8, kSectionId2,
+                                     /*namespace_id=*/0);
+  EXPECT_THAT(edit8.BufferTerm("term-one", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit8.BufferTerm("term-three", TermMatchType::EXACT_ONLY),
+              IsOk());
+  EXPECT_THAT(edit8.BufferTerm("term-five", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit8.IndexAllBufferedTerms(), IsOk());
 
   // verify the combination of lite index and main index is in correct order.
@@ -2091,23 +2083,20 @@
 }
 
 TEST_F(IndexTest, FindTermByPrefix_InTermMatchTypePrefix_ShouldReturnInOrder) {
-  Index::Editor edit1 =
-      index_->Edit(kDocumentId0, kSectionId2, TermMatchType::PREFIX,
-                   /*namespace_id=*/0);
+  Index::Editor edit1 = index_->Edit(kDocumentId0, kSectionId2,
+                                     /*namespace_id=*/0);
   AlwaysTrueSuggestionResultCheckerImpl impl;
-  EXPECT_THAT(edit1.BufferTerm("fo"), IsOk());
+  EXPECT_THAT(edit1.BufferTerm("fo", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit1.IndexAllBufferedTerms(), IsOk());
 
-  Index::Editor edit2 =
-      index_->Edit(kDocumentId2, kSectionId2, TermMatchType::PREFIX,
-                   /*namespace_id=*/0);
-  EXPECT_THAT(edit2.BufferTerm("foo"), IsOk());
+  Index::Editor edit2 = index_->Edit(kDocumentId2, kSectionId2,
+                                     /*namespace_id=*/0);
+  EXPECT_THAT(edit2.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit2.IndexAllBufferedTerms(), IsOk());
 
-  Index::Editor edit3 =
-      index_->Edit(kDocumentId3, kSectionId2, TermMatchType::PREFIX,
-                   /*namespace_id=*/0);
-  EXPECT_THAT(edit3.BufferTerm("fool"), IsOk());
+  Index::Editor edit3 = index_->Edit(kDocumentId3, kSectionId2,
+                                     /*namespace_id=*/0);
+  EXPECT_THAT(edit3.BufferTerm("fool", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit3.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK(index_->Merge());
@@ -2136,41 +2125,40 @@
 }
 
 TEST_F(IndexTest, FindTermByPrefixShouldReturnHitCountForMain) {
-  Index::Editor edit =
-      index_->Edit(kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/0);
+  Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
+                                    /*namespace_id=*/0);
   AlwaysTrueSuggestionResultCheckerImpl impl;
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  edit = index_->Edit(kDocumentId1, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId1, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId2, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId2, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId3, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId3, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId4, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId4, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId5, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId5, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId6, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId6, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId7, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId7, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   // 'foo' has 1 hit, 'fool' has 8 hits.
@@ -2194,19 +2182,18 @@
 }
 
 TEST_F(IndexTest, FindTermByPrefixShouldReturnCombinedHitCount) {
-  Index::Editor edit =
-      index_->Edit(kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/0);
+  Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
+                                    /*namespace_id=*/0);
   AlwaysTrueSuggestionResultCheckerImpl impl;
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK(index_->Merge());
 
-  edit = index_->Edit(kDocumentId1, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId1, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   EXPECT_THAT(
@@ -2219,21 +2206,19 @@
 }
 
 TEST_F(IndexTest, FindTermRankComparison) {
-  Index::Editor edit =
-      index_->Edit(kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/0);
+  Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
+                                    /*namespace_id=*/0);
   AlwaysTrueSuggestionResultCheckerImpl impl;
-  EXPECT_THAT(edit.BufferTerm("fo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("fo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
-  Index::Editor edit2 =
-      index_->Edit(kDocumentId2, kSectionId2, TermMatchType::PREFIX,
-                   /*namespace_id=*/0);
-  EXPECT_THAT(edit2.BufferTerm("fo"), IsOk());
-  EXPECT_THAT(edit2.BufferTerm("foo"), IsOk());
+  Index::Editor edit2 = index_->Edit(kDocumentId2, kSectionId2,
+                                     /*namespace_id=*/0);
+  EXPECT_THAT(edit2.BufferTerm("fo", TermMatchType::PREFIX), IsOk());
+  EXPECT_THAT(edit2.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit2.IndexAllBufferedTerms(), IsOk());
 
   EXPECT_THAT(
@@ -2288,19 +2273,18 @@
 }
 
 TEST_F(IndexTest, FindTermByPrefixShouldReturnTermsFromBothIndices) {
-  Index::Editor edit =
-      index_->Edit(kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY,
-                   /*namespace_id=*/0);
+  Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
+                                    /*namespace_id=*/0);
   AlwaysTrueSuggestionResultCheckerImpl impl;
 
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK(index_->Merge());
 
-  edit = index_->Edit(kDocumentId1, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId1, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   // 'foo' has 1 hit in the main index, 'fool' has 1 hit in the lite index.
@@ -2319,9 +2303,9 @@
   EXPECT_THAT(size, Eq(0));
 
   // Add an element.
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   ICING_ASSERT_OK_AND_ASSIGN(size, index_->GetElementsSize());
   EXPECT_THAT(size, Gt(0));
@@ -2332,24 +2316,24 @@
 }
 
 TEST_F(IndexTest, ExactResultsFromLiteAndMain) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId1, kSectionId3, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId1, kSectionId3,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foot"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foot", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   ICING_ASSERT_OK(index_->Merge());
 
-  edit = index_->Edit(kDocumentId2, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId2, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("footer"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("footer", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId2, kSectionId3, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId2, kSectionId3,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -2365,24 +2349,24 @@
 }
 
 TEST_F(IndexTest, PrefixResultsFromLiteAndMain) {
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
-  EXPECT_THAT(edit.BufferTerm("fool"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  EXPECT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId1, kSectionId3, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId1, kSectionId3,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foot"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foot", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   ICING_ASSERT_OK(index_->Merge());
 
-  edit = index_->Edit(kDocumentId2, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId2, kSectionId2,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("footer"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("footer", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId2, kSectionId3, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId2, kSectionId3,
                       /*namespace_id=*/0);
-  EXPECT_THAT(edit.BufferTerm("foo"), IsOk());
+  EXPECT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -2401,26 +2385,26 @@
 TEST_F(IndexTest, GetDebugInfo) {
   // Add two documents to the lite index, merge them into the main index and
   // then add another doc to the lite index.
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
-  ASSERT_THAT(edit.BufferTerm("fool"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  ASSERT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId1, kSectionId3, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId1, kSectionId3,
                       /*namespace_id=*/0);
   index_->set_last_added_document_id(kDocumentId1);
-  ASSERT_THAT(edit.BufferTerm("foot"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("foot", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   ICING_ASSERT_OK(index_->Merge());
 
-  edit = index_->Edit(kDocumentId2, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId2, kSectionId2,
                       /*namespace_id=*/0);
   index_->set_last_added_document_id(kDocumentId2);
-  ASSERT_THAT(edit.BufferTerm("footer"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("footer", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId2, kSectionId3, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId2, kSectionId3,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   IndexDebugInfoProto out0 = index_->GetDebugInfo(DebugInfoVerbosity::BASIC);
@@ -2438,10 +2422,10 @@
               SizeIs(Gt(out0.lite_index_info().size())));
 
   // Add one more doc to the lite index. Debug strings should change.
-  edit = index_->Edit(kDocumentId3, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId3, kSectionId2,
                       /*namespace_id=*/0);
   index_->set_last_added_document_id(kDocumentId3);
-  ASSERT_THAT(edit.BufferTerm("far"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("far", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   IndexDebugInfoProto out2 = index_->GetDebugInfo(DebugInfoVerbosity::BASIC);
@@ -2468,17 +2452,17 @@
 TEST_F(IndexTest, BackfillingMultipleTermsSucceeds) {
   // Add two documents to the lite index, merge them into the main index and
   // then add another doc to the lite index.
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId0, kSectionId3, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId0, kSectionId3,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("fool"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("fool", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId1, kSectionId3, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId1, kSectionId3,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foot"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("foot", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   // After this merge the index should have posting lists for
@@ -2488,9 +2472,9 @@
   ICING_ASSERT_OK(index_->Merge());
 
   // Add one more doc to the lite index.
-  edit = index_->Edit(kDocumentId2, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId2, kSectionId2,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("far"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("far", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   // After this merge the index should add a posting list for "far" and a
@@ -2517,14 +2501,14 @@
 TEST_F(IndexTest, BackfillingNewTermsSucceeds) {
   // Add two documents to the lite index, merge them into the main index and
   // then add another doc to the lite index.
-  Index::Editor edit = index_->Edit(
-      kDocumentId0, kSectionId2, TermMatchType::EXACT_ONLY, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
-  ASSERT_THAT(edit.BufferTerm("fool"), IsOk());
+  Index::Editor edit =
+      index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::EXACT_ONLY), IsOk());
+  ASSERT_THAT(edit.BufferTerm("fool", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId1, kSectionId3, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId1, kSectionId3,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foot"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("foot", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   // After this merge the index should have posting lists for
   // "fool" {(doc0,sec2)},
@@ -2532,18 +2516,18 @@
   // "foo"  {(doc1,sec3),(doc0,sec2)}
   ICING_ASSERT_OK(index_->Merge());
 
-  edit = index_->Edit(kDocumentId2, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId2, kSectionId2,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("footer"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("footer", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId2, kSectionId3, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId2, kSectionId3,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   // Add one more doc to the lite index. Debug strings should change.
-  edit = index_->Edit(kDocumentId3, kSectionId2, TermMatchType::EXACT_ONLY,
+  edit = index_->Edit(kDocumentId3, kSectionId2,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("far"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("far", TermMatchType::EXACT_ONLY), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   // After this merge the index should add posting lists for "far" and "footer"
@@ -2582,8 +2566,8 @@
 
   // Add one document to the lite index
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   // Clipping to invalid should have no effect.
   ICING_EXPECT_OK(index_->TruncateTo(kInvalidDocumentId));
@@ -2606,9 +2590,9 @@
               ElementsAre(EqualsDocHitInfo(
                   kDocumentId0, std::vector<SectionId>{kSectionId2})));
 
-  edit = index_->Edit(kDocumentId1, kSectionId3, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId1, kSectionId3,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foot"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("foot", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   // Clipping to invalid should still have no effect even if both indices have
@@ -2637,8 +2621,8 @@
 
   // Add one document to the lite index
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   index_->set_last_added_document_id(kDocumentId0);
   ICING_EXPECT_OK(index_->TruncateTo(index_->last_added_document_id()));
@@ -2662,9 +2646,9 @@
               ElementsAre(EqualsDocHitInfo(
                   kDocumentId0, std::vector<SectionId>{kSectionId2})));
 
-  edit = index_->Edit(kDocumentId1, kSectionId3, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId1, kSectionId3,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foot"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("foot", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   index_->set_last_added_document_id(kDocumentId1);
 
@@ -2685,17 +2669,17 @@
 TEST_F(IndexTest, TruncateToThrowsOutLiteIndex) {
   // Add one document to the lite index and merge it into main.
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   index_->set_last_added_document_id(kDocumentId0);
 
   ICING_ASSERT_OK(index_->Merge());
 
   // Add another document to the lite index.
-  edit = index_->Edit(kDocumentId1, kSectionId3, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId1, kSectionId3,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foot"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("foot", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   index_->set_last_added_document_id(kDocumentId1);
 
@@ -2715,22 +2699,22 @@
 TEST_F(IndexTest, TruncateToThrowsOutBothIndices) {
   // Add two documents to the lite index and merge them into main.
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   index_->set_last_added_document_id(kDocumentId0);
-  edit = index_->Edit(kDocumentId1, kSectionId2, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId1, kSectionId2,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foul"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("foul", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   index_->set_last_added_document_id(kDocumentId1);
 
   ICING_ASSERT_OK(index_->Merge());
 
   // Add another document to the lite index.
-  edit = index_->Edit(kDocumentId2, kSectionId3, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId2, kSectionId3,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foot"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("foot", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
   index_->set_last_added_document_id(kDocumentId2);
 
@@ -2748,13 +2732,13 @@
 TEST_F(IndexTest, IndexStorageInfoProto) {
   // Add two documents to the lite index and merge them into main.
   {
-    Index::Editor edit = index_->Edit(
-        kDocumentId0, kSectionId2, TermMatchType::PREFIX, /*namespace_id=*/0);
-    ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
+    Index::Editor edit =
+        index_->Edit(kDocumentId0, kSectionId2, /*namespace_id=*/0);
+    ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
     EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-    edit = index_->Edit(kDocumentId1, kSectionId2, TermMatchType::PREFIX,
+    edit = index_->Edit(kDocumentId1, kSectionId2,
                         /*namespace_id=*/0);
-    ASSERT_THAT(edit.BufferTerm("foul"), IsOk());
+    ASSERT_THAT(edit.BufferTerm("foul", TermMatchType::PREFIX), IsOk());
     EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
     ICING_ASSERT_OK(index_->Merge());
@@ -2776,12 +2760,12 @@
 TEST_F(IndexTest, PublishQueryStats) {
   // Add two documents to the lite index without merging.
   Index::Editor edit = index_->Edit(kDocumentId0, kSectionId2,
-                                    TermMatchType::PREFIX, /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foo"), IsOk());
+                                    /*namespace_id=*/0);
+  ASSERT_THAT(edit.BufferTerm("foo", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
-  edit = index_->Edit(kDocumentId1, kSectionId2, TermMatchType::PREFIX,
+  edit = index_->Edit(kDocumentId1, kSectionId2,
                       /*namespace_id=*/0);
-  ASSERT_THAT(edit.BufferTerm("foul"), IsOk());
+  ASSERT_THAT(edit.BufferTerm("foul", TermMatchType::PREFIX), IsOk());
   EXPECT_THAT(edit.IndexAllBufferedTerms(), IsOk());
 
   // Verify query stats.
diff --git a/icing/index/integer-section-indexing-handler.cc b/icing/index/integer-section-indexing-handler.cc
index 63b09df..bf9cdd8 100644
--- a/icing/index/integer-section-indexing-handler.cc
+++ b/icing/index/integer-section-indexing-handler.cc
@@ -47,7 +47,8 @@
 
 libtextclassifier3::Status IntegerSectionIndexingHandler::Handle(
     const TokenizedDocument& tokenized_document, DocumentId document_id,
-    bool recovery_mode, PutDocumentStatsProto* put_document_stats) {
+    DocumentId /*old_document_id*/ _, bool recovery_mode,
+    PutDocumentStatsProto* put_document_stats) {
   std::unique_ptr<Timer> index_timer = clock_.GetNewTimer();
 
   if (!IsDocumentIdValid(document_id)) {
diff --git a/icing/index/integer-section-indexing-handler.h b/icing/index/integer-section-indexing-handler.h
index 0a501aa..b6e1dc2 100644
--- a/icing/index/integer-section-indexing-handler.h
+++ b/icing/index/integer-section-indexing-handler.h
@@ -47,6 +47,9 @@
   // Handles the integer indexing process: add hits into the integer index for
   // all contents in tokenized_document.integer_sections.
   //
+  // Parameter old_document_id is unused since there is no need to migrate data
+  // from old_document_id to (new) document_id.
+  //
   // Returns:
   //   - OK on success.
   //   - INVALID_ARGUMENT_ERROR if document_id is invalid OR document_id is less
@@ -55,7 +58,8 @@
   //   - Any NumericIndex<int64_t>::Editor errors.
   libtextclassifier3::Status Handle(
       const TokenizedDocument& tokenized_document, DocumentId document_id,
-      bool recovery_mode, PutDocumentStatsProto* put_document_stats) override;
+      DocumentId /*old_document_id*/ _, bool recovery_mode,
+      PutDocumentStatsProto* put_document_stats) override;
 
  private:
   explicit IntegerSectionIndexingHandler(const Clock* clock,
diff --git a/icing/index/integer-section-indexing-handler_test.cc b/icing/index/integer-section-indexing-handler_test.cc
index 4da05f9..00b435b 100644
--- a/icing/index/integer-section-indexing-handler_test.cc
+++ b/icing/index/integer-section-indexing-handler_test.cc
@@ -14,6 +14,7 @@
 
 #include "icing/index/integer-section-indexing-handler.h"
 
+#include <cstdint>
 #include <limits>
 #include <memory>
 #include <string>
@@ -25,6 +26,7 @@
 #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/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
@@ -40,11 +42,12 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.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/tokenized-document.h"
 #include "unicode/uloc.h"
 
@@ -88,10 +91,11 @@
 class IntegerSectionIndexingHandlerTest : public ::testing::Test {
  protected:
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
 
@@ -118,8 +122,8 @@
         filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str()),
         IsTrue());
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     SchemaProto schema =
         SchemaBuilder()
             .AddType(
@@ -167,13 +171,12 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult doc_store_create_result,
         DocumentStore::Create(&filesystem_, document_store_dir_, &fake_clock_,
-                              schema_store_.get(),
+                              schema_store_.get(), feature_flags_.get(),
                               /*force_recovery_and_revalidate_documents=*/false,
-                              /*namespace_id_fingerprint=*/true,
                               /*pre_mapping_fbv=*/false,
                               /*use_persistent_hash_map=*/true,
                               PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDeflateCompressionLevel,
+                                  DocumentWrapper>::kDefaultCompressionLevel,
                               /*initialize_stats=*/nullptr));
     document_store_ = std::move(doc_store_create_result.document_store);
   }
@@ -187,6 +190,7 @@
     filesystem_.DeleteDirectoryRecursively(base_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   FakeClock fake_clock_;
   std::string base_dir_;
@@ -234,7 +238,6 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
       document_store_->Put(tokenized_document.document()));
-  DocumentId document_id = put_result.new_document_id;
 
   ASSERT_THAT(integer_index_->last_added_document_id(), Eq(kInvalidDocumentId));
   // Handle document.
@@ -243,10 +246,12 @@
       IntegerSectionIndexingHandler::Create(&fake_clock_,
                                             integer_index_.get()));
   EXPECT_THAT(
-      handler->Handle(tokenized_document, document_id, /*recovery_mode=*/false,
+      handler->Handle(tokenized_document, put_result.new_document_id,
+                      put_result.old_document_id, /*recovery_mode=*/false,
                       /*put_document_stats=*/nullptr),
       IsOk());
-  EXPECT_THAT(integer_index_->last_added_document_id(), Eq(document_id));
+  EXPECT_THAT(integer_index_->last_added_document_id(),
+              Eq(put_result.new_document_id));
 
   // Query "timestamp".
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -257,7 +262,8 @@
           *schema_store_, fake_clock_.GetSystemTimeMilliseconds()));
   EXPECT_THAT(GetHits(std::move(itr)),
               ElementsAre(EqualsDocHitInfo(
-                  document_id, std::vector<SectionId>{kSectionIdTimestamp})));
+                  put_result.new_document_id,
+                  std::vector<SectionId>{kSectionIdTimestamp})));
 }
 
 TEST_F(IntegerSectionIndexingHandlerTest, HandleNestedIntegerSection) {
@@ -284,7 +290,6 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
       document_store_->Put(tokenized_document.document()));
-  DocumentId document_id = put_result.new_document_id;
 
   ASSERT_THAT(integer_index_->last_added_document_id(), Eq(kInvalidDocumentId));
   // Handle nested_document.
@@ -293,10 +298,12 @@
       IntegerSectionIndexingHandler::Create(&fake_clock_,
                                             integer_index_.get()));
   EXPECT_THAT(
-      handler->Handle(tokenized_document, document_id, /*recovery_mode=*/false,
+      handler->Handle(tokenized_document, put_result.new_document_id,
+                      put_result.old_document_id, /*recovery_mode=*/false,
                       /*put_document_stats=*/nullptr),
       IsOk());
-  EXPECT_THAT(integer_index_->last_added_document_id(), Eq(document_id));
+  EXPECT_THAT(integer_index_->last_added_document_id(),
+              Eq(put_result.new_document_id));
 
   // Query "nested.timestamp".
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -305,10 +312,10 @@
           "nested.timestamp", /*key_lower=*/std::numeric_limits<int64_t>::min(),
           /*key_upper=*/std::numeric_limits<int64_t>::max(), *document_store_,
           *schema_store_, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(
-      GetHits(std::move(itr)),
-      ElementsAre(EqualsDocHitInfo(
-          document_id, std::vector<SectionId>{kSectionIdNestedTimestamp})));
+  EXPECT_THAT(GetHits(std::move(itr)),
+              ElementsAre(EqualsDocHitInfo(
+                  put_result.new_document_id,
+                  std::vector<SectionId>{kSectionIdNestedTimestamp})));
 
   // Query "price".
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -317,9 +324,10 @@
           kPropertyPrice, /*key_lower=*/std::numeric_limits<int64_t>::min(),
           /*key_upper=*/std::numeric_limits<int64_t>::max(), *document_store_,
           *schema_store_, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(GetHits(std::move(itr)),
-              ElementsAre(EqualsDocHitInfo(
-                  document_id, std::vector<SectionId>{kSectionIdPrice})));
+  EXPECT_THAT(
+      GetHits(std::move(itr)),
+      ElementsAre(EqualsDocHitInfo(put_result.new_document_id,
+                                   std::vector<SectionId>{kSectionIdPrice})));
 
   // Query "timestamp". Should get empty result.
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -347,7 +355,6 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
       document_store_->Put(tokenized_document.document()));
-  DocumentId document_id = put_result.new_document_id;
 
   ASSERT_THAT(integer_index_->last_added_document_id(), Eq(kInvalidDocumentId));
   // Handle document. Index data should remain unchanged since there is no
@@ -357,10 +364,12 @@
       IntegerSectionIndexingHandler::Create(&fake_clock_,
                                             integer_index_.get()));
   EXPECT_THAT(
-      handler->Handle(tokenized_document, document_id, /*recovery_mode=*/false,
+      handler->Handle(tokenized_document, put_result.new_document_id,
+                      put_result.old_document_id, /*recovery_mode=*/false,
                       /*put_document_stats=*/nullptr),
       IsOk());
-  EXPECT_THAT(integer_index_->last_added_document_id(), Eq(document_id));
+  EXPECT_THAT(integer_index_->last_added_document_id(),
+              Eq(put_result.new_document_id));
 
   // Query "timestamp". Should get empty result.
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -373,7 +382,7 @@
 }
 
 TEST_F(IntegerSectionIndexingHandlerTest,
-       HandleInvalidDocumentIdShouldReturnInvalidArgumentError) {
+       HandleInvalidNewDocumentIdShouldReturnInvalidArgumentError) {
   DocumentProto document =
       DocumentBuilder()
           .SetKey("icing", "fake_type/1")
@@ -401,6 +410,7 @@
   // index data and last_added_document_id should remain unchanged.
   EXPECT_THAT(
       handler->Handle(tokenized_document, kInvalidDocumentId,
+                      /*old_document_id=*/kInvalidDocumentId,
                       /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(integer_index_->last_added_document_id(), Eq(kCurrentDocumentId));
@@ -417,6 +427,7 @@
   // Recovery mode should get the same result.
   EXPECT_THAT(
       handler->Handle(tokenized_document, kInvalidDocumentId,
+                      /*old_document_id=*/kInvalidDocumentId,
                       /*recovery_mode=*/true, /*put_document_stats=*/nullptr),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(integer_index_->last_added_document_id(), Eq(kCurrentDocumentId));
@@ -448,7 +459,6 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
       document_store_->Put(tokenized_document.document()));
-  DocumentId document_id = put_result.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<IntegerSectionIndexingHandler> handler,
@@ -458,13 +468,16 @@
   // Handling document with document_id == last_added_document_id should cause a
   // failure, and both index data and last_added_document_id should remain
   // unchanged.
-  integer_index_->set_last_added_document_id(document_id);
-  ASSERT_THAT(integer_index_->last_added_document_id(), Eq(document_id));
+  integer_index_->set_last_added_document_id(put_result.new_document_id);
+  ASSERT_THAT(integer_index_->last_added_document_id(),
+              Eq(put_result.new_document_id));
   EXPECT_THAT(
-      handler->Handle(tokenized_document, document_id, /*recovery_mode=*/false,
+      handler->Handle(tokenized_document, put_result.new_document_id,
+                      put_result.old_document_id, /*recovery_mode=*/false,
                       /*put_document_stats=*/nullptr),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
-  EXPECT_THAT(integer_index_->last_added_document_id(), Eq(document_id));
+  EXPECT_THAT(integer_index_->last_added_document_id(),
+              Eq(put_result.new_document_id));
 
   // Query "timestamp". Should get empty result.
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -478,13 +491,16 @@
   // Handling document with document_id < last_added_document_id should cause a
   // failure, and both index data and last_added_document_id should remain
   // unchanged.
-  integer_index_->set_last_added_document_id(document_id + 1);
-  ASSERT_THAT(integer_index_->last_added_document_id(), Eq(document_id + 1));
+  integer_index_->set_last_added_document_id(put_result.new_document_id + 1);
+  ASSERT_THAT(integer_index_->last_added_document_id(),
+              Eq(put_result.new_document_id + 1));
   EXPECT_THAT(
-      handler->Handle(tokenized_document, document_id, /*recovery_mode=*/false,
+      handler->Handle(tokenized_document, put_result.new_document_id,
+                      put_result.old_document_id, /*recovery_mode=*/false,
                       /*put_document_stats=*/nullptr),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
-  EXPECT_THAT(integer_index_->last_added_document_id(), Eq(document_id + 1));
+  EXPECT_THAT(integer_index_->last_added_document_id(),
+              Eq(put_result.new_document_id + 1));
 
   // Query "timestamp". Should get empty result.
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -525,11 +541,9 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
       document_store_->Put(tokenized_document1.document()));
-  DocumentId document_id1 = put_result1.new_document_id;
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
       document_store_->Put(tokenized_document2.document()));
-  DocumentId document_id2 = put_result2.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<IntegerSectionIndexingHandler> handler,
@@ -539,10 +553,12 @@
   // Handle document with document_id > last_added_document_id in recovery mode.
   // The handler should index this document and update last_added_document_id.
   EXPECT_THAT(
-      handler->Handle(tokenized_document1, document_id1, /*recovery_mode=*/true,
+      handler->Handle(tokenized_document1, put_result1.new_document_id,
+                      put_result1.old_document_id, /*recovery_mode=*/true,
                       /*put_document_stats=*/nullptr),
       IsOk());
-  EXPECT_THAT(integer_index_->last_added_document_id(), Eq(document_id1));
+  EXPECT_THAT(integer_index_->last_added_document_id(),
+              Eq(put_result1.new_document_id));
 
   // Query "timestamp".
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -553,19 +569,23 @@
           *schema_store_, fake_clock_.GetSystemTimeMilliseconds()));
   EXPECT_THAT(GetHits(std::move(itr)),
               ElementsAre(EqualsDocHitInfo(
-                  document_id1, std::vector<SectionId>{kSectionIdTimestamp})));
+                  put_result1.new_document_id,
+                  std::vector<SectionId>{kSectionIdTimestamp})));
 
   // Handle document with document_id == last_added_document_id in recovery
   // mode. We should not get any error, but the handler should ignore the
   // document, so both index data and last_added_document_id should remain
   // unchanged.
-  integer_index_->set_last_added_document_id(document_id2);
-  ASSERT_THAT(integer_index_->last_added_document_id(), Eq(document_id2));
+  integer_index_->set_last_added_document_id(put_result2.new_document_id);
+  ASSERT_THAT(integer_index_->last_added_document_id(),
+              Eq(put_result2.new_document_id));
   EXPECT_THAT(
-      handler->Handle(tokenized_document2, document_id2, /*recovery_mode=*/true,
+      handler->Handle(tokenized_document2, put_result2.new_document_id,
+                      put_result2.old_document_id, /*recovery_mode=*/true,
                       /*put_document_stats=*/nullptr),
       IsOk());
-  EXPECT_THAT(integer_index_->last_added_document_id(), Eq(document_id2));
+  EXPECT_THAT(integer_index_->last_added_document_id(),
+              Eq(put_result2.new_document_id));
 
   // Query "timestamp". Should not get hits for document2.
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -576,18 +596,22 @@
           *schema_store_, fake_clock_.GetSystemTimeMilliseconds()));
   EXPECT_THAT(GetHits(std::move(itr)),
               ElementsAre(EqualsDocHitInfo(
-                  document_id1, std::vector<SectionId>{kSectionIdTimestamp})));
+                  put_result1.new_document_id,
+                  std::vector<SectionId>{kSectionIdTimestamp})));
 
   // Handle document with document_id < last_added_document_id in recovery mode.
   // We should not get any error, but the handler should ignore the document, so
   // both index data and last_added_document_id should remain unchanged.
-  integer_index_->set_last_added_document_id(document_id2 + 1);
-  ASSERT_THAT(integer_index_->last_added_document_id(), Eq(document_id2 + 1));
+  integer_index_->set_last_added_document_id(put_result2.new_document_id + 1);
+  ASSERT_THAT(integer_index_->last_added_document_id(),
+              Eq(put_result2.new_document_id + 1));
   EXPECT_THAT(
-      handler->Handle(tokenized_document2, document_id2, /*recovery_mode=*/true,
+      handler->Handle(tokenized_document2, put_result2.new_document_id,
+                      put_result2.old_document_id, /*recovery_mode=*/true,
                       /*put_document_stats=*/nullptr),
       IsOk());
-  EXPECT_THAT(integer_index_->last_added_document_id(), Eq(document_id2 + 1));
+  EXPECT_THAT(integer_index_->last_added_document_id(),
+              Eq(put_result2.new_document_id + 1));
 
   // Query "timestamp". Should not get hits for document2.
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -598,7 +622,8 @@
           *schema_store_, fake_clock_.GetSystemTimeMilliseconds()));
   EXPECT_THAT(GetHits(std::move(itr)),
               ElementsAre(EqualsDocHitInfo(
-                  document_id1, std::vector<SectionId>{kSectionIdTimestamp})));
+                  put_result1.new_document_id,
+                  std::vector<SectionId>{kSectionIdTimestamp})));
 }
 
 }  // namespace
diff --git a/icing/index/iterator/doc-hit-info-iterator-by-uri.cc b/icing/index/iterator/doc-hit-info-iterator-by-uri.cc
new file mode 100644
index 0000000..c5a9ecc
--- /dev/null
+++ b/icing/index/iterator/doc-hit-info-iterator-by-uri.cc
@@ -0,0 +1,108 @@
+// Copyright (C) 2024 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/iterator/doc-hit-info-iterator-by-uri.h"
+
+#include <algorithm>
+#include <functional>
+#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/absl_ports/canonical_errors.h"
+#include "icing/absl_ports/str_cat.h"
+#include "icing/index/hit/doc-hit-info.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<DocHitInfoIteratorByUri>>
+DocHitInfoIteratorByUri::Create(const DocumentStore* document_store,
+                                const SearchSpecProto& search_spec) {
+  ICING_RETURN_ERROR_IF_NULL(document_store);
+
+  if (search_spec.document_uri_filters().empty()) {
+    return absl_ports::InvalidArgumentError(
+        "Cannot create DocHitInfoIteratorByUri with empty "
+        "document_uri_filters");
+  }
+
+  std::unordered_set<std::string> all_namespaces(
+      search_spec.namespace_filters().begin(),
+      search_spec.namespace_filters().end());
+  std::vector<DocumentId> target_document_ids;
+  for (const NamespaceDocumentUriGroup& namespace_document_uri_group :
+       search_spec.document_uri_filters()) {
+    const std::string& current_namespace =
+        namespace_document_uri_group.namespace_();
+    if (!all_namespaces.empty() &&
+        all_namespaces.count(current_namespace) == 0) {
+      // The current namespace doesn't appear in the namespace filter.
+      return absl_ports::InvalidArgumentError(absl_ports::StrCat(
+          "The namespace : ", current_namespace,
+          " appears in the document uri filter, but does not appear in the "
+          "namespace filter."));
+    }
+
+    if (namespace_document_uri_group.document_uris().empty()) {
+      return absl_ports::InvalidArgumentError(absl_ports::StrCat(
+          "The namespace ", current_namespace,
+          " has not specified any document in the document uri filter. Please "
+          "use the namespace filter to exclude a namespace instead."));
+    }
+    for (const std::string& document_uri :
+         namespace_document_uri_group.document_uris()) {
+      auto document_id_or =
+          document_store->GetDocumentId(current_namespace, document_uri);
+      if (!document_id_or.ok()) {
+        continue;
+      }
+      target_document_ids.push_back(document_id_or.ValueOrDie());
+    }
+  }
+  // Sort the document ids in descending order, which is a requirement for all
+  // iterators.
+  std::sort(target_document_ids.begin(), target_document_ids.end(),
+            std::greater<DocumentId>());
+  // Deduplicate the document ids.
+  auto last_itr =
+      std::unique(target_document_ids.begin(), target_document_ids.end());
+  target_document_ids.erase(last_itr, target_document_ids.end());
+
+  return std::unique_ptr<DocHitInfoIteratorByUri>(
+      new DocHitInfoIteratorByUri(std::move(target_document_ids)));
+}
+
+libtextclassifier3::Status DocHitInfoIteratorByUri::Advance() {
+  current_document_id_index_++;
+
+  if (current_document_id_index_ >= target_document_ids_.size()) {
+    doc_hit_info_ = DocHitInfo(kInvalidDocumentId);
+    return absl_ports::ResourceExhaustedError(
+        "No more DocHitInfos in iterator");
+  }
+  doc_hit_info_.set_document_id(
+      target_document_ids_[current_document_id_index_]);
+  return libtextclassifier3::Status::OK;
+}
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/index/iterator/doc-hit-info-iterator-by-uri.h b/icing/index/iterator/doc-hit-info-iterator-by-uri.h
new file mode 100644
index 0000000..d136535
--- /dev/null
+++ b/icing/index/iterator/doc-hit-info-iterator-by-uri.h
@@ -0,0 +1,82 @@
+// Copyright (C) 2024 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_BY_URI_H_
+#define ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_BY_URI_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/absl_ports/canonical_errors.h"
+#include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/proto/search.pb.h"
+#include "icing/store/document-id.h"
+#include "icing/store/document-store.h"
+
+namespace icing {
+namespace lib {
+
+class DocHitInfoIteratorByUri : public DocHitInfoIterator {
+ public:
+  // Creates a DocHitInfoIteratorByUri based on the given search_spec.
+  //
+  // Returns:
+  //   - A DocHitInfoIteratorByUri instance on success.
+  //   - INVALID_ARGUMENT error if the search_spec's document_uri_filters is
+  //     empty or if any namespace in the document_uri_filters has no document
+  //     URIs specified.
+  static libtextclassifier3::StatusOr<std::unique_ptr<DocHitInfoIteratorByUri>>
+  Create(const DocumentStore* document_store,
+         const SearchSpecProto& search_spec);
+
+  libtextclassifier3::Status Advance() override;
+
+  libtextclassifier3::StatusOr<TrimmedNode> TrimRightMostNode() && override {
+    return absl_ports::InvalidArgumentError(
+        "DocHitInfoIteratorByUri should not be used in suggestion.");
+  }
+
+  void MapChildren(const ChildrenMapper& mapper) override {}
+
+  CallStats GetCallStats() const override {
+    return 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=*/num_advance_calls_,
+        /*num_blocks_inspected_in=*/0);
+  }
+
+  std::string ToString() const override { return "uri_iterator"; }
+
+ private:
+  explicit DocHitInfoIteratorByUri(std::vector<DocumentId> target_document_ids)
+      : target_document_ids_(std::move(target_document_ids)),
+        current_document_id_index_(-1),
+        num_advance_calls_(0) {}
+
+  std::vector<DocumentId> target_document_ids_;
+  int current_document_id_index_;
+
+  int num_advance_calls_;
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_BY_URI_H_
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
new file mode 100644
index 0000000..a11bf9e
--- /dev/null
+++ b/icing/index/iterator/doc-hit-info-iterator-by-uri_test.cc
@@ -0,0 +1,368 @@
+// Copyright (C) 2024 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/iterator/doc-hit-info-iterator-by-uri.h"
+
+#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/feature-flags.h"
+#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/proto/document.pb.h"
+#include "icing/proto/schema.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-feature-flags.h"
+#include "icing/testing/tmp-directory.h"
+
+namespace icing {
+namespace lib {
+
+namespace {
+
+using ::testing::ElementsAre;
+using ::testing::IsEmpty;
+
+class DocHitInfoIteratorByUriTest : public ::testing::Test {
+ protected:
+  DocHitInfoIteratorByUriTest() : test_dir_(GetTestTempDir() + "/icing") {}
+
+  void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+
+    filesystem_.CreateDirectoryRecursively(test_dir_.c_str());
+
+    SchemaProto schema =
+        SchemaBuilder()
+            .AddType(SchemaTypeConfigBuilder().SetType("email"))
+            .Build();
+    ICING_ASSERT_OK_AND_ASSIGN(
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           &fake_clock_, feature_flags_.get()));
+    ICING_ASSERT_OK(schema_store_->SetSchema(
+        schema, /*ignore_errors_and_delete_documents=*/false,
+        /*allow_circular_schema_definitions=*/false));
+
+    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));
+    document_store_ = std::move(create_result.document_store);
+  }
+
+  void TearDown() override {
+    document_store_.reset();
+    schema_store_.reset();
+    filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
+  }
+
+  std::unique_ptr<FeatureFlags> feature_flags_;
+  std::unique_ptr<SchemaStore> schema_store_;
+  std::unique_ptr<DocumentStore> document_store_;
+  FakeClock fake_clock_;
+  const Filesystem filesystem_;
+  const std::string test_dir_;
+};
+
+TEST_F(DocHitInfoIteratorByUriTest, EmptyFilterIsInvalid) {
+  // Create a search spec without a uri filter specified.
+  SearchSpecProto search_spec;
+  EXPECT_THAT(
+      DocHitInfoIteratorByUri::Create(document_store_.get(), search_spec),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+
+  // Add a namespace group with no uris.
+  NamespaceDocumentUriGroup* namespace_uris =
+      search_spec.add_document_uri_filters();
+  namespace_uris->set_namespace_("namespace");
+  EXPECT_THAT(
+      DocHitInfoIteratorByUri::Create(document_store_.get(), search_spec),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+TEST_F(DocHitInfoIteratorByUriTest, MatchesSomeDocuments) {
+  // Put documents
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("namespace", "email/1")
+                                .SetSchema("email")
+                                .Build();
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("namespace", "email/2")
+                                .SetSchema("email")
+                                .Build();
+  DocumentProto document3 = DocumentBuilder()
+                                .SetKey("namespace", "email/3")
+                                .SetSchema("email")
+                                .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             document_store_->Put(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));
+  DocumentId document_id3 = put_result.new_document_id;
+
+  // Create a search spec with uri filters that only match document1 and
+  // document3.
+  SearchSpecProto search_spec;
+  NamespaceDocumentUriGroup* uris = search_spec.add_document_uri_filters();
+  uris->set_namespace_("namespace");
+  uris->add_document_uris("email/1");
+  uris->add_document_uris("email/3");
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocHitInfoIteratorByUri> iterator,
+      DocHitInfoIteratorByUri::Create(document_store_.get(), search_spec));
+  EXPECT_THAT(GetDocumentIds(iterator.get()),
+              ElementsAre(document_id3, document_id1));
+  EXPECT_FALSE(iterator->Advance().ok());
+}
+
+TEST_F(DocHitInfoIteratorByUriTest, MatchesAllDocuments) {
+  // Put documents
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("namespace", "email/1")
+                                .SetSchema("email")
+                                .Build();
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("namespace", "email/2")
+                                .SetSchema("email")
+                                .Build();
+  DocumentProto document3 = DocumentBuilder()
+                                .SetKey("namespace", "email/3")
+                                .SetSchema("email")
+                                .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             document_store_->Put(document1));
+  DocumentId document_id1 = put_result.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document2));
+  DocumentId document_id2 = put_result.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document3));
+  DocumentId document_id3 = put_result.new_document_id;
+
+  // Create a search spec with uri filters that match all documents.
+  SearchSpecProto search_spec;
+  NamespaceDocumentUriGroup* uris = search_spec.add_document_uri_filters();
+  uris->set_namespace_("namespace");
+  uris->add_document_uris("email/1");
+  uris->add_document_uris("email/2");
+  uris->add_document_uris("email/3");
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocHitInfoIteratorByUri> iterator,
+      DocHitInfoIteratorByUri::Create(document_store_.get(), search_spec));
+  EXPECT_THAT(GetDocumentIds(iterator.get()),
+              ElementsAre(document_id3, document_id2, document_id1));
+  EXPECT_FALSE(iterator->Advance().ok());
+}
+
+TEST_F(DocHitInfoIteratorByUriTest, NonexistentUriIsOk) {
+  // Put documents
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("namespace", "email/1")
+                                .SetSchema("email")
+                                .Build();
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("namespace", "email/2")
+                                .SetSchema("email")
+                                .Build();
+  DocumentProto document3 = DocumentBuilder()
+                                .SetKey("namespace", "email/3")
+                                .SetSchema("email")
+                                .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             document_store_->Put(document1));
+  DocumentId document_id1 = put_result.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document2));
+  DocumentId document_id2 = put_result.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document3));
+  DocumentId document_id3 = put_result.new_document_id;
+
+  // Create a search spec with a nonexistent uri in uri filters.
+  SearchSpecProto search_spec;
+  NamespaceDocumentUriGroup* uris = search_spec.add_document_uri_filters();
+  uris->set_namespace_("namespace");
+  uris->add_document_uris("email/1");
+  uris->add_document_uris("email/2");
+  uris->add_document_uris("email/3");
+  uris->add_document_uris("nonexistent_uri");
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocHitInfoIteratorByUri> iterator,
+      DocHitInfoIteratorByUri::Create(document_store_.get(), search_spec));
+  EXPECT_THAT(GetDocumentIds(iterator.get()),
+              ElementsAre(document_id3, document_id2, document_id1));
+  EXPECT_FALSE(iterator->Advance().ok());
+}
+
+TEST_F(DocHitInfoIteratorByUriTest, AllNonexistentUriShouldReturnEmptyResults) {
+  // Put documents
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("namespace", "email/1")
+                                .SetSchema("email")
+                                .Build();
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("namespace", "email/2")
+                                .SetSchema("email")
+                                .Build();
+  DocumentProto document3 = DocumentBuilder()
+                                .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));
+
+  // Create a search spec with all nonexistent uris.
+  SearchSpecProto search_spec;
+  NamespaceDocumentUriGroup* uris = search_spec.add_document_uri_filters();
+  uris->set_namespace_("namespace");
+  uris->add_document_uris("nonexistent_uri1");
+  uris->add_document_uris("nonexistent_uri2");
+  uris->add_document_uris("nonexistent_uri3");
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocHitInfoIteratorByUri> iterator,
+      DocHitInfoIteratorByUri::Create(document_store_.get(), search_spec));
+  EXPECT_THAT(GetDocumentIds(iterator.get()), IsEmpty());
+  EXPECT_FALSE(iterator->Advance().ok());
+}
+
+TEST_F(DocHitInfoIteratorByUriTest, MultipleNamespaces) {
+  // Put documents
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("namespace1", "email/1")
+                                .SetSchema("email")
+                                .Build();
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("namespace1", "email/2")
+                                .SetSchema("email")
+                                .Build();
+  DocumentProto document3 = DocumentBuilder()
+                                .SetKey("namespace2", "email/3")
+                                .SetSchema("email")
+                                .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             document_store_->Put(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));
+  DocumentId document_id3 = put_result.new_document_id;
+
+  // Create a search spec with uri filters that match document1 and document3 in
+  // different namespaces.
+  SearchSpecProto search_spec;
+  NamespaceDocumentUriGroup* namespace1_uris =
+      search_spec.add_document_uri_filters();
+  namespace1_uris->set_namespace_("namespace1");
+  namespace1_uris->add_document_uris("email/1");
+  NamespaceDocumentUriGroup* namespace2_uris =
+      search_spec.add_document_uri_filters();
+  namespace2_uris->set_namespace_("namespace2");
+  namespace2_uris->add_document_uris("email/3");
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocHitInfoIteratorByUri> iterator,
+      DocHitInfoIteratorByUri::Create(document_store_.get(), search_spec));
+  EXPECT_THAT(GetDocumentIds(iterator.get()),
+              ElementsAre(document_id3, document_id1));
+  EXPECT_FALSE(iterator->Advance().ok());
+}
+
+TEST_F(DocHitInfoIteratorByUriTest, DuplicatedUriIsOk) {
+  // Put documents
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("namespace", "email/1")
+                                .SetSchema("email")
+                                .Build();
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("namespace", "email/2")
+                                .SetSchema("email")
+                                .Build();
+  DocumentProto document3 = DocumentBuilder()
+                                .SetKey("namespace", "email/3")
+                                .SetSchema("email")
+                                .Build();
+  DocumentProto document4 = DocumentBuilder()
+                                .SetKey("namespace", "email/4")
+                                .SetSchema("email")
+                                .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             document_store_->Put(document1));
+  DocumentId document_id1 = put_result.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document2));
+  DocumentId document_id2 = put_result.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document3));
+  DocumentId document_id3 = put_result.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(put_result, document_store_->Put(document4));
+  DocumentId document_id4 = put_result.new_document_id;
+
+  // Create a search spec with duplicated uri filters. The result document ids
+  // should be de-duplicated.
+  SearchSpecProto search_spec;
+  NamespaceDocumentUriGroup* uris = search_spec.add_document_uri_filters();
+  uris->set_namespace_("namespace");
+  uris->add_document_uris("email/1");
+  uris->add_document_uris("email/2");
+  uris->add_document_uris("email/3");
+  uris->add_document_uris("email/3");
+
+  uris = search_spec.add_document_uri_filters();
+  uris->set_namespace_("namespace");
+  uris->add_document_uris("email/2");
+  uris->add_document_uris("email/4");
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocHitInfoIteratorByUri> iterator,
+      DocHitInfoIteratorByUri::Create(document_store_.get(), search_spec));
+  EXPECT_THAT(
+      GetDocumentIds(iterator.get()),
+      ElementsAre(document_id4, document_id3, document_id2, document_id1));
+  EXPECT_FALSE(iterator->Advance().ok());
+}
+
+TEST_F(DocHitInfoIteratorByUriTest, TrimRightMostNodeResultsInError) {
+  SearchSpecProto search_spec;
+  NamespaceDocumentUriGroup* uris = search_spec.add_document_uri_filters();
+  uris->set_namespace_("namespace");
+  uris->add_document_uris("uri");
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<DocHitInfoIteratorByUri> iterator,
+      DocHitInfoIteratorByUri::Create(document_store_.get(), search_spec));
+  EXPECT_THAT(std::move(*iterator).TrimRightMostNode(),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
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 b088212..38a628c 100644
--- a/icing/index/iterator/doc-hit-info-iterator-filter_test.cc
+++ b/icing/index/iterator/doc-hit-info-iterator-filter_test.cc
@@ -25,6 +25,7 @@
 #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/index/hit/doc-hit-info.h"
@@ -41,6 +42,7 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
+#include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 #include "icing/util/clock.h"
 
@@ -55,13 +57,13 @@
 
 libtextclassifier3::StatusOr<DocumentStore::CreateResult> CreateDocumentStore(
     const Filesystem* filesystem, const std::string& base_dir,
-    const Clock* clock, const SchemaStore* schema_store) {
+    const Clock* clock, const SchemaStore* schema_store,
+    const FeatureFlags& feature_flags) {
   return DocumentStore::Create(
-      filesystem, base_dir, clock, schema_store,
+      filesystem, base_dir, clock, schema_store, &feature_flags,
       /*force_recovery_and_revalidate_documents=*/false,
-      /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-      /*use_persistent_hash_map=*/true,
-      PortableFileBackedProtoLog<DocumentWrapper>::kDeflateCompressionLevel,
+      /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
+      PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
       /*initialize_stats=*/nullptr);
 }
 
@@ -71,6 +73,7 @@
       : test_dir_(GetTestTempDir() + "/icing") {}
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     filesystem_.CreateDirectoryRecursively(test_dir_.c_str());
     test_document1_ =
         DocumentBuilder().SetKey("icing", "email/1").SetSchema("email").Build();
@@ -84,8 +87,8 @@
             .AddType(SchemaTypeConfigBuilder().SetType("email"))
             .Build();
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, test_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     ICING_ASSERT_OK(schema_store_->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -93,7 +96,7 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
         CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                            schema_store_.get()));
+                            schema_store_.get(), *feature_flags_));
     document_store_ = std::move(create_result.document_store);
   }
 
@@ -105,6 +108,7 @@
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   std::unique_ptr<SchemaStore> schema_store_;
   std::unique_ptr<DocumentStore> document_store_;
   FakeClock fake_clock_;
@@ -238,6 +242,7 @@
       : test_dir_(GetTestTempDir() + "/icing") {}
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     filesystem_.CreateDirectoryRecursively(test_dir_.c_str());
     document1_namespace1_ = DocumentBuilder()
                                 .SetKey(namespace1_, "email/1")
@@ -261,8 +266,8 @@
             .AddType(SchemaTypeConfigBuilder().SetType("email"))
             .Build();
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, test_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     ICING_ASSERT_OK(schema_store_->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -270,7 +275,7 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
         CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                            schema_store_.get()));
+                            schema_store_.get(), *feature_flags_));
     document_store_ = std::move(create_result.document_store);
   }
 
@@ -282,6 +287,7 @@
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   std::unique_ptr<SchemaStore> schema_store_;
   std::unique_ptr<DocumentStore> document_store_;
   FakeClock fake_clock_;
@@ -428,6 +434,7 @@
       : test_dir_(GetTestTempDir() + "/icing") {}
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     filesystem_.CreateDirectoryRecursively(test_dir_.c_str());
     document1_schema1_ = DocumentBuilder()
                              .SetKey("namespace", "1")
@@ -459,8 +466,8 @@
                          .AddParentType(kSchema2))
             .Build();
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, test_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     ICING_ASSERT_OK(schema_store_->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -468,7 +475,7 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
         CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                            schema_store_.get()));
+                            schema_store_.get(), *feature_flags_));
     document_store_ = std::move(create_result.document_store);
   }
 
@@ -480,6 +487,7 @@
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   std::unique_ptr<SchemaStore> schema_store_;
   std::unique_ptr<DocumentStore> document_store_;
   FakeClock fake_clock_;
@@ -735,6 +743,7 @@
       : test_dir_(GetTestTempDir() + "/icing") {}
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     filesystem_.CreateDirectoryRecursively(test_dir_.c_str());
 
     SchemaProto schema =
@@ -742,8 +751,8 @@
             .AddType(SchemaTypeConfigBuilder().SetType(email_schema_))
             .Build();
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, test_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     ICING_ASSERT_OK(schema_store_->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -751,7 +760,7 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
         CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                            schema_store_.get()));
+                            schema_store_.get(), *feature_flags_));
     document_store_ = std::move(create_result.document_store);
   }
 
@@ -763,6 +772,7 @@
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   std::unique_ptr<SchemaStore> schema_store_;
   std::unique_ptr<DocumentStore> document_store_;
   FakeClock fake_clock_;
@@ -779,7 +789,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
@@ -812,7 +822,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
@@ -845,7 +855,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
@@ -879,7 +889,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
@@ -910,6 +920,7 @@
   DocHitInfoIteratorFilterTest() : test_dir_(GetTestTempDir() + "/icing") {}
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     filesystem_.CreateDirectoryRecursively(test_dir_.c_str());
     document1_namespace1_schema1_ = DocumentBuilder()
                                         .SetKey(namespace1_, "1")
@@ -948,8 +959,8 @@
             .AddType(SchemaTypeConfigBuilder().SetType(schema2_))
             .Build();
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, test_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     ICING_ASSERT_OK(schema_store_->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -957,7 +968,7 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
         CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                            schema_store_.get()));
+                            schema_store_.get(), *feature_flags_));
     document_store_ = std::move(create_result.document_store);
   }
 
@@ -969,6 +980,7 @@
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   std::unique_ptr<SchemaStore> schema_store_;
   std::unique_ptr<DocumentStore> document_store_;
   FakeClock fake_clock_;
@@ -992,7 +1004,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
diff --git a/icing/index/iterator/doc-hit-info-iterator-match-score-expression.cc b/icing/index/iterator/doc-hit-info-iterator-match-score-expression.cc
new file mode 100644
index 0000000..8c94b16
--- /dev/null
+++ b/icing/index/iterator/doc-hit-info-iterator-match-score-expression.cc
@@ -0,0 +1,54 @@
+// Copyright (C) 2024 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/iterator/doc-hit-info-iterator-match-score-expression.h"
+
+#include <memory>
+#include <string_view>
+
+#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/hit/doc-hit-info.h"
+#include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/scoring/advanced_scoring/score-expression.h"
+#include "icing/store/document-id.h"
+
+namespace icing {
+namespace lib {
+
+libtextclassifier3::Status DocHitInfoIteratorMatchScoreExpression::Advance() {
+  while (delegate_->Advance().ok()) {
+    libtextclassifier3::StatusOr<double> score_or =
+        scoring_expression_->EvaluateDouble(delegate_->doc_hit_info(),
+                                            /*query_it=*/nullptr);
+    if (!score_or.ok()) {
+      continue;
+    }
+    double score = score_or.ValueOrDie();
+    if (score < score_low_ || score > score_high_) {
+      // Score is outside of the desired range, skip this result.
+      continue;
+    }
+    doc_hit_info_ = delegate_->doc_hit_info();
+    return libtextclassifier3::Status::OK;
+  }
+
+  // Didn't find anything on the delegate iterator.
+  doc_hit_info_ = DocHitInfo(kInvalidDocumentId);
+  return absl_ports::ResourceExhaustedError("No more DocHitInfos in iterator");
+}
+
+}  // namespace lib
+}  // namespace icing
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
new file mode 100644
index 0000000..f46b8da
--- /dev/null
+++ b/icing/index/iterator/doc-hit-info-iterator-match-score-expression.h
@@ -0,0 +1,99 @@
+// Copyright (C) 2024 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_MATCH_SCORE_EXPRESSION_H_
+#define ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_MATCH_SCORE_EXPRESSION_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/absl_ports/canonical_errors.h"
+#include "icing/absl_ports/str_cat.h"
+#include "icing/index/iterator/doc-hit-info-iterator-all-document-id.h"
+#include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/schema/section.h"
+#include "icing/scoring/advanced_scoring/score-expression.h"
+#include "icing/store/document-id.h"
+
+namespace icing {
+namespace lib {
+
+class DocHitInfoIteratorMatchScoreExpression : public DocHitInfoIterator {
+ public:
+  explicit DocHitInfoIteratorMatchScoreExpression(
+      DocumentId last_added_document_id,
+      std::unique_ptr<ScoreExpression> scoring_expression, double score_low,
+      double score_high)
+      : delegate_(std::make_unique<DocHitInfoIteratorAllDocumentId>(
+            last_added_document_id)),
+        scoring_expression_(std::move(scoring_expression)),
+        score_low_(score_low),
+        score_high_(score_high) {};
+
+  libtextclassifier3::Status Advance() override;
+
+  libtextclassifier3::StatusOr<TrimmedNode> TrimRightMostNode() && override {
+    // TODO(b/377215223): Decide on the correct behavior for this function when
+    // supporting query language optimizations and the delegate is not
+    // DocHitInfoIteratorAllDocumentId.
+    return absl_ports::InvalidArgumentError(
+        "Query suggestions for the matchScoreExpression function are not "
+        "supported");
+  }
+
+  void MapChildren(const ChildrenMapper& mapper) override {
+    delegate_ = mapper(std::move(delegate_));
+  }
+
+  CallStats GetCallStats() const override { return delegate_->GetCallStats(); }
+
+  std::string ToString() const override {
+    // TODO(b/377215223): Consider having a ToString method for ScoreExpression,
+    // so that it can be displayed here.
+    return absl_ports::StrCat("(matchScoreExpression: ", delegate_->ToString(),
+                              ")");
+  }
+
+  void PopulateMatchedTermsStats(
+      std::vector<TermMatchInfo>* matched_terms_stats,
+      SectionIdMask filtering_section_mask = kSectionIdMaskAll) const override {
+    // TODO(b/377215223): Decide on the correct behavior for this function when
+    // supporting query language optimizations and the delegate is not
+    // DocHitInfoIteratorAllDocumentId.
+  }
+
+ private:
+  // TODO(b/377215223): Currently, the delegate can only be
+  // DocHitInfoIteratorAllDocumentId, but filtering on all documents is
+  // inefficient. Consider the following optimizations:
+  // 1. Implement a query language optimization so that queries like "foo AND
+  //    matchScoreExpression(...)" can be optimized by letting
+  //    DocHitInfoIteratorMatchScoreExpression filter on "foo".
+  // 2. Apply other filter iterators at the bottom level instead of the top
+  //    level, similar to how section restrictions are handled.
+  std::unique_ptr<DocHitInfoIterator> delegate_;
+
+  std::unique_ptr<ScoreExpression> scoring_expression_;
+  double score_low_;
+  double score_high_;
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_INDEX_ITERATOR_DOC_HIT_INFO_ITERATOR_MATCH_SCORE_EXPRESSION_H_
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 17bd55d..6fb84ce 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
@@ -22,7 +22,9 @@
 #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/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator-all-document-id.h"
 #include "icing/index/iterator/doc-hit-info-iterator-test-util.h"
@@ -36,6 +38,7 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
+#include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 
 namespace icing {
@@ -53,6 +56,7 @@
       : test_dir_(GetTestTempDir() + "/icing") {}
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     filesystem_.CreateDirectoryRecursively(test_dir_.c_str());
     document1_ = DocumentBuilder()
                      .SetKey("namespace", "uri1")
@@ -89,22 +93,22 @@
             .Build();
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, test_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     ICING_ASSERT_OK(schema_store_->SetSchema(
         schema_, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(
-            &filesystem_, test_dir_, &fake_clock_, schema_store_.get(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
   }
 
@@ -114,6 +118,7 @@
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   std::unique_ptr<SchemaStore> schema_store_;
   std::unique_ptr<DocumentStore> document_store_;
   const Filesystem filesystem_;
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 94e9b5e..34e206e 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
@@ -23,7 +23,9 @@
 #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/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator-and.h"
 #include "icing/index/iterator/doc-hit-info-iterator-test-util.h"
@@ -38,6 +40,7 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
+#include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 
 namespace icing {
@@ -58,6 +61,7 @@
       : test_dir_(GetTestTempDir() + "/icing") {}
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     filesystem_.CreateDirectoryRecursively(test_dir_.c_str());
     document1_ = DocumentBuilder()
                      .SetKey("namespace", "uri1")
@@ -94,22 +98,22 @@
             .Build();
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, test_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     ICING_ASSERT_OK(schema_store_->SetSchema(
         schema_, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(
-            &filesystem_, test_dir_, &fake_clock_, schema_store_.get(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
   }
 
@@ -119,6 +123,7 @@
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   std::unique_ptr<SchemaStore> schema_store_;
   std::unique_ptr<DocumentStore> document_store_;
   const Filesystem filesystem_;
diff --git a/icing/index/lite/lite-index.cc b/icing/index/lite/lite-index.cc
index 98a34c4..cd03dd8 100644
--- a/icing/index/lite/lite-index.cc
+++ b/icing/index/lite/lite-index.cc
@@ -308,7 +308,7 @@
 }
 
 libtextclassifier3::StatusOr<uint32_t> LiteIndex::InsertTerm(
-    const std::string& term, TermMatchType::Code term_match_type,
+    std::string_view term, TermMatchType::Code term_match_type,
     NamespaceId namespace_id) {
   absl_ports::unique_lock l(&mutex_);
   uint32_t tvi;
@@ -366,7 +366,7 @@
 }
 
 libtextclassifier3::StatusOr<uint32_t> LiteIndex::GetTermId(
-    const std::string& term) const {
+    std::string_view term) const {
   absl_ports::shared_lock l(&mutex_);
   char dummy;
   uint32_t tvi;
@@ -686,8 +686,11 @@
       // below if there are any valid hits pointing to that termid.
       tvi_to_delete.insert(curr_tvi);
     }
+    DocumentId old_document_id = term_id_hit_pair.hit().document_id();
     DocumentId new_document_id =
-        document_id_old_to_new[term_id_hit_pair.hit().document_id()];
+        old_document_id >= 0 && old_document_id < document_id_old_to_new.size()
+            ? document_id_old_to_new[old_document_id]
+            : kInvalidDocumentId;
     if (new_document_id == kInvalidDocumentId) {
       continue;
     }
@@ -704,6 +707,15 @@
     // allocated region of hit_buffer_.
     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
+      // error here.
+      ICING_LOG(ERROR)
+          << "GetMutableMem failed in Optimize. This should never happen.";
+      return absl_ports::ResourceExhaustedError(
+          "Allocating more space in hit buffer failed!");
+    }
     *valp = new_term_id_hit_pair.value();
   }
   header_->set_cur_size(new_size);
diff --git a/icing/index/lite/lite-index.h b/icing/index/lite/lite-index.h
index 149fbea..280df88 100644
--- a/icing/index/lite/lite-index.h
+++ b/icing/index/lite/lite-index.h
@@ -96,8 +96,8 @@
   libtextclassifier3::Status PersistToDisk() ICING_LOCKS_EXCLUDED(mutex_);
 
   // Returns term_id if term found, NOT_FOUND otherwise.
-  libtextclassifier3::StatusOr<uint32_t> GetTermId(
-      const std::string& term) const ICING_LOCKS_EXCLUDED(mutex_);
+  libtextclassifier3::StatusOr<uint32_t> GetTermId(std::string_view term) const
+      ICING_LOCKS_EXCLUDED(mutex_);
 
   // Returns an iterator for all terms for which 'prefix' is a prefix.
   class PrefixIterator {
@@ -130,7 +130,7 @@
   //   A value index on success
   //   RESOURCE_EXHAUSTED if lexicon is full or no disk space is available
   libtextclassifier3::StatusOr<uint32_t> InsertTerm(
-      const std::string& term, TermMatchType::Code term_match_type,
+      std::string_view term, TermMatchType::Code term_match_type,
       NamespaceId namespace_id) ICING_LOCKS_EXCLUDED(mutex_);
 
   // Updates term properties by setting hasPrefixHits and namespace id of the
diff --git a/icing/index/numeric/integer-index-storage.cc b/icing/index/numeric/integer-index-storage.cc
index 22ed842..6a00bf1 100644
--- a/icing/index/numeric/integer-index-storage.cc
+++ b/icing/index/numeric/integer-index-storage.cc
@@ -716,9 +716,11 @@
                              old_pl_accessor->GetNextDataBatch());
       while (!batch_old_data.empty()) {
         for (const IntegerIndexData& old_data : batch_old_data) {
+          DocumentId old_document_id = old_data.basic_hit().document_id();
           DocumentId new_document_id =
-              old_data.basic_hit().document_id() < document_id_old_to_new.size()
-                  ? document_id_old_to_new[old_data.basic_hit().document_id()]
+              old_document_id >= 0 &&
+                      old_document_id < document_id_old_to_new.size()
+                  ? document_id_old_to_new[old_document_id]
                   : kInvalidDocumentId;
           // Transfer the document id of the hit if the document is not deleted
           // or outdated.
diff --git a/icing/index/numeric/integer-index_test.cc b/icing/index/numeric/integer-index_test.cc
index 4563b5b..7bc684b 100644
--- a/icing/index/numeric/integer-index_test.cc
+++ b/icing/index/numeric/integer-index_test.cc
@@ -27,7 +27,9 @@
 #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/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
 #include "icing/index/numeric/dummy-numeric-index.h"
@@ -37,9 +39,12 @@
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/schema-builder.h"
+#include "icing/schema/schema-store.h"
 #include "icing/schema/section.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"
 
 namespace icing {
@@ -68,6 +73,7 @@
 class NumericIndexIntegerTest : public ::testing::Test {
  protected:
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     base_dir_ = GetTestTempDir() + "/icing";
     ASSERT_THAT(filesystem_.CreateDirectoryRecursively(base_dir_.c_str()),
                 IsTrue());
@@ -76,22 +82,24 @@
     std::string schema_dir = base_dir_ + "/schema_test";
 
     ASSERT_TRUE(filesystem_.CreateDirectoryRecursively(schema_dir.c_str()));
+
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_, SchemaStore::Create(&filesystem_, schema_dir, &clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_dir, &clock_,
+                                           feature_flags_.get()));
 
     std::string document_store_dir = base_dir_ + "/doc_store_test";
     ASSERT_TRUE(
         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(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     doc_store_ = std::move(doc_store_create_result.document_store);
   }
 
@@ -159,14 +167,14 @@
 
     ICING_ASSIGN_OR_RETURN(
         DocumentStore::CreateResult doc_store_create_result,
-        DocumentStore::Create(
-            &filesystem_, document_store_dir, &clock_, schema_store_.get(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*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);
   }
@@ -187,6 +195,7 @@
     return result;
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   std::string base_dir_;
   std::string working_path_;
diff --git a/icing/index/property-existence-indexing-handler.cc b/icing/index/property-existence-indexing-handler.cc
index 504f380..0b4d87f 100644
--- a/icing/index/property-existence-indexing-handler.cc
+++ b/icing/index/property-existence-indexing-handler.cc
@@ -81,6 +81,7 @@
 
 libtextclassifier3::Status PropertyExistenceIndexingHandler::Handle(
     const TokenizedDocument& tokenized_document, DocumentId document_id,
+    DocumentId /*old_document_id*/ _,
     PutDocumentStatsProto* put_document_stats) {
   std::unique_ptr<Timer> index_timer = clock_.GetNewTimer();
 
@@ -88,13 +89,12 @@
   // Section id is irrelevant to metadata tokens that is used to support
   // property existence check.
   Index::Editor editor =
-      index_.Edit(document_id, /*section_id=*/0, TermMatchType::EXACT_ONLY,
-                  /*namespace_id=*/0);
+      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);
   for (const std::string& meta_token : meta_tokens) {
-    status = editor.BufferTerm(meta_token.c_str());
+    status = editor.BufferTerm(meta_token, TermMatchType::EXACT_ONLY);
     if (!status.ok()) {
       // We've encountered a failure. Bail out. We'll mark this doc as deleted
       // and signal a failure to the client.
diff --git a/icing/index/property-existence-indexing-handler.h b/icing/index/property-existence-indexing-handler.h
index 55c0bb4..cfdc3d8 100644
--- a/icing/index/property-existence-indexing-handler.h
+++ b/icing/index/property-existence-indexing-handler.h
@@ -63,6 +63,9 @@
   // - kPropertyExistenceTokenPrefix + "propC"
   // - kPropertyExistenceTokenPrefix + "propC.propD"
   //
+  // Parameter old_document_id is unused since there is no need to migrate data
+  // from old_document_id to (new) document_id.
+  //
   /// Returns:
   //   - OK on success
   //   - RESOURCE_EXHAUSTED_ERROR if the index is full and can't add anymore
@@ -70,6 +73,7 @@
   //   - INTERNAL_ERROR if any other errors occur.
   libtextclassifier3::Status Handle(const TokenizedDocument& tokenized_document,
                                     DocumentId document_id,
+                                    DocumentId /*old_document_id*/ _,
                                     PutDocumentStatsProto* put_document_stats);
 
  private:
diff --git a/icing/index/property-existence-indexing-handler_test.cc b/icing/index/property-existence-indexing-handler_test.cc
index 3228734..653bf0c 100644
--- a/icing/index/property-existence-indexing-handler_test.cc
+++ b/icing/index/property-existence-indexing-handler_test.cc
@@ -28,6 +28,7 @@
 #include "gtest/gtest.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/portable-file-backed-proto-log.h"
 #include "icing/index/hit/doc-hit-info.h"
@@ -46,13 +47,14 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.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/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "icing/util/tokenized-document.h"
 #include "unicode/uloc.h"
 
@@ -78,10 +80,11 @@
 class PropertyExistenceIndexingHandlerTest : public Test {
  protected:
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
 
@@ -106,9 +109,10 @@
     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_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     SchemaProto schema =
         SchemaBuilder()
             .AddType(
@@ -157,13 +161,12 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult doc_store_create_result,
         DocumentStore::Create(&filesystem_, document_store_dir_, &fake_clock_,
-                              schema_store_.get(),
+                              schema_store_.get(), feature_flags_.get(),
                               /*force_recovery_and_revalidate_documents=*/false,
-                              /*namespace_id_fingerprint=*/true,
                               /*pre_mapping_fbv=*/false,
                               /*use_persistent_hash_map=*/true,
                               PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDeflateCompressionLevel,
+                                  DocumentWrapper>::kDefaultCompressionLevel,
                               /*initialize_stats=*/nullptr));
     document_store_ = std::move(doc_store_create_result.document_store);
   }
@@ -177,6 +180,7 @@
     filesystem_.DeleteDirectoryRecursively(base_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   IcingFilesystem icing_filesystem_;
   FakeClock fake_clock_;
@@ -274,12 +278,15 @@
 
   // Handle all docs
   EXPECT_THAT(handler->Handle(tokenized_document0, document_id0,
+                              put_result0.old_document_id,
                               /*put_document_stats=*/nullptr),
               IsOk());
   EXPECT_THAT(handler->Handle(tokenized_document1, document_id1,
+                              put_result1.old_document_id,
                               /*put_document_stats=*/nullptr),
               IsOk());
   EXPECT_THAT(handler->Handle(tokenized_document2, document_id2,
+                              put_result0.old_document_id,
                               /*put_document_stats=*/nullptr),
               IsOk());
 
@@ -383,6 +390,7 @@
       std::unique_ptr<PropertyExistenceIndexingHandler> handler,
       PropertyExistenceIndexingHandler::Create(&fake_clock_, index.get()));
   EXPECT_THAT(handler->Handle(tokenized_root_document, document_id,
+                              put_result.old_document_id,
                               /*put_document_stats=*/nullptr),
               IsOk());
 
@@ -507,12 +515,15 @@
 
   // Handle all docs
   EXPECT_THAT(handler->Handle(tokenized_document0, document_id0,
+                              put_result0.old_document_id,
                               /*put_document_stats=*/nullptr),
               IsOk());
   EXPECT_THAT(handler->Handle(tokenized_document1, document_id1,
+                              put_result1.old_document_id,
                               /*put_document_stats=*/nullptr),
               IsOk());
   EXPECT_THAT(handler->Handle(tokenized_document2, document_id2,
+                              put_result2.old_document_id,
                               /*put_document_stats=*/nullptr),
               IsOk());
 
diff --git a/icing/index/string-section-indexing-handler.cc b/icing/index/string-section-indexing-handler.cc
index 8b20d04..438b51f 100644
--- a/icing/index/string-section-indexing-handler.cc
+++ b/icing/index/string-section-indexing-handler.cc
@@ -16,7 +16,6 @@
 
 #include <cstdint>
 #include <memory>
-#include <string>
 #include <string_view>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
@@ -47,6 +46,7 @@
 
 libtextclassifier3::Status StringSectionIndexingHandler::Handle(
     const TokenizedDocument& tokenized_document, DocumentId document_id,
+    DocumentId /*old_document_id*/ _,
     PutDocumentStatsProto* put_document_stats) {
   uint32_t num_tokens = 0;
   libtextclassifier3::Status status;
@@ -59,17 +59,13 @@
     }
     // TODO(b/152934343): pass real namespace ids in
     Index::Editor editor =
-        index_.Edit(document_id, section.metadata.id,
-                    section.metadata.term_match_type, /*namespace_id=*/0);
+        index_.Edit(document_id, section.metadata.id, /*namespace_id=*/0);
     for (std::string_view token : section.token_sequence) {
       ++num_tokens;
 
       switch (section.metadata.tokenizer) {
         case StringIndexingConfig::TokenizerType::VERBATIM:
-          // data() is safe to use here because a token created from the
-          // VERBATIM tokenizer is the entire string value. The character at
-          // data() + token.length() is guaranteed to be a null char.
-          status = editor.BufferTerm(token.data());
+          status = editor.BufferTerm(token, section.metadata.term_match_type);
           break;
         case StringIndexingConfig::TokenizerType::NONE:
           [[fallthrough]];
@@ -78,8 +74,10 @@
         case StringIndexingConfig::TokenizerType::URL:
           [[fallthrough]];
         case StringIndexingConfig::TokenizerType::PLAIN:
-          std::string normalized_term = normalizer_.NormalizeTerm(token);
-          status = editor.BufferTerm(normalized_term.c_str());
+          Normalizer::NormalizedTerm normalized_term =
+              normalizer_.NormalizeTerm(token);
+          status = editor.BufferTerm(normalized_term.text,
+                                     section.metadata.term_match_type);
       }
 
       if (!status.ok()) {
diff --git a/icing/index/string-section-indexing-handler.h b/icing/index/string-section-indexing-handler.h
index 8452e9f..b644c65 100644
--- a/icing/index/string-section-indexing-handler.h
+++ b/icing/index/string-section-indexing-handler.h
@@ -52,6 +52,9 @@
   // all contents in tokenized_document.tokenized_string_sections and merge lite
   // index into main index if necessary.
   //
+  // Parameter old_document_id is unused since there is no need to migrate data
+  // from old_document_id to (new) document_id.
+  //
   /// Returns:
   //   - OK on success
   //   - RESOURCE_EXHAUSTED_ERROR if the index is full and can't add anymore
@@ -60,6 +63,7 @@
   //   - Any main/lite index errors.
   libtextclassifier3::Status Handle(const TokenizedDocument& tokenized_document,
                                     DocumentId document_id,
+                                    DocumentId /*old_document_id*/ _,
                                     PutDocumentStatsProto* put_document_stats);
 
  private:
diff --git a/icing/index/term-indexing-handler.cc b/icing/index/term-indexing-handler.cc
index 7eb9dda..24b57a1 100644
--- a/icing/index/term-indexing-handler.cc
+++ b/icing/index/term-indexing-handler.cc
@@ -65,7 +65,8 @@
 
 libtextclassifier3::Status TermIndexingHandler::Handle(
     const TokenizedDocument& tokenized_document, DocumentId document_id,
-    bool recovery_mode, PutDocumentStatsProto* put_document_stats) {
+    DocumentId old_document_id, bool recovery_mode,
+    PutDocumentStatsProto* put_document_stats) {
   std::unique_ptr<Timer> index_timer = clock_.GetNewTimer();
 
   if (index_.last_added_document_id() != kInvalidDocumentId &&
@@ -84,11 +85,11 @@
   libtextclassifier3::Status status = libtextclassifier3::Status::OK;
   if (property_existence_indexing_handler_ != nullptr) {
     status = property_existence_indexing_handler_->Handle(
-        tokenized_document, document_id, put_document_stats);
+        tokenized_document, document_id, old_document_id, put_document_stats);
   }
   if (status.ok()) {
     status = string_section_indexing_handler_->Handle(
-        tokenized_document, document_id, put_document_stats);
+        tokenized_document, document_id, old_document_id, put_document_stats);
   }
 
   if (put_document_stats != nullptr) {
diff --git a/icing/index/term-indexing-handler.h b/icing/index/term-indexing-handler.h
index c055bbf..79e407a 100644
--- a/icing/index/term-indexing-handler.h
+++ b/icing/index/term-indexing-handler.h
@@ -56,7 +56,7 @@
   // - Sorts the lite index if necessary.
   // - Merges the lite index into the main index if necessary.
   //
-  /// Returns:
+  // Returns:
   //   - OK on success
   //   - INVALID_ARGUMENT_ERROR if document_id is less than or equal to the
   //     document_id of a previously indexed document in non recovery mode.
@@ -68,7 +68,8 @@
   //   - Any main/lite index errors.
   libtextclassifier3::Status Handle(
       const TokenizedDocument& tokenized_document, DocumentId document_id,
-      bool recovery_mode, PutDocumentStatsProto* put_document_stats) override;
+      DocumentId old_document_id, bool recovery_mode,
+      PutDocumentStatsProto* put_document_stats) override;
 
  private:
   explicit TermIndexingHandler(const Clock* clock, Index* index,
diff --git a/icing/index/term-indexing-handler_test.cc b/icing/index/term-indexing-handler_test.cc
index 7b97f88..a82541f 100644
--- a/icing/index/term-indexing-handler_test.cc
+++ b/icing/index/term-indexing-handler_test.cc
@@ -29,6 +29,7 @@
 #include "gtest/gtest.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/portable-file-backed-proto-log.h"
 #include "icing/index/hit/doc-hit-info.h"
@@ -50,13 +51,14 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.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/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "icing/util/tokenized-document.h"
 #include "unicode/uloc.h"
 
@@ -99,10 +101,11 @@
 class TermIndexingHandlerTest : public Test {
  protected:
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
 
@@ -128,8 +131,8 @@
         filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str()),
         IsTrue());
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     SchemaProto schema =
         SchemaBuilder()
             .AddType(
@@ -174,13 +177,12 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult doc_store_create_result,
         DocumentStore::Create(&filesystem_, document_store_dir_, &fake_clock_,
-                              schema_store_.get(),
+                              schema_store_.get(), feature_flags_.get(),
                               /*force_recovery_and_revalidate_documents=*/false,
-                              /*namespace_id_fingerprint=*/true,
                               /*pre_mapping_fbv=*/false,
                               /*use_persistent_hash_map=*/true,
                               PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDeflateCompressionLevel,
+                                  DocumentWrapper>::kDefaultCompressionLevel,
                               /*initialize_stats=*/nullptr));
     document_store_ = std::move(doc_store_create_result.document_store);
   }
@@ -194,6 +196,7 @@
     filesystem_.DeleteDirectoryRecursively(base_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   IcingFilesystem icing_filesystem_;
   FakeClock fake_clock_;
@@ -273,10 +276,10 @@
       TermIndexingHandler::Create(
           &fake_clock_, normalizer_.get(), index.get(),
           /*build_property_existence_metadata_hits=*/true));
-  EXPECT_THAT(
-      handler->Handle(tokenized_document, document_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
-      IsOk());
+  EXPECT_THAT(handler->Handle(
+                  tokenized_document, document_id, put_result.old_document_id,
+                  /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
+              IsOk());
 
   EXPECT_THAT(index->last_added_document_id(), Eq(document_id));
 
@@ -338,10 +341,10 @@
       TermIndexingHandler::Create(
           &fake_clock_, normalizer_.get(), index.get(),
           /*build_property_existence_metadata_hits=*/true));
-  EXPECT_THAT(
-      handler->Handle(tokenized_document, document_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
-      IsOk());
+  EXPECT_THAT(handler->Handle(
+                  tokenized_document, document_id, put_result.old_document_id,
+                  /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
+              IsOk());
 
   EXPECT_THAT(index->last_added_document_id(), Eq(document_id));
 
@@ -436,22 +439,25 @@
 
   // Handle doc0 and doc1. The LiteIndex should sort and merge after adding
   // these
-  EXPECT_THAT(handler->Handle(tokenized_document0, document_id0,
-                              /*recovery_mode=*/false,
-                              /*put_document_stats=*/nullptr),
-              IsOk());
-  EXPECT_THAT(handler->Handle(tokenized_document1, document_id1,
-                              /*recovery_mode=*/false,
-                              /*put_document_stats=*/nullptr),
-              IsOk());
+  EXPECT_THAT(
+      handler->Handle(tokenized_document0, document_id0,
+                      put_result0.old_document_id, /*recovery_mode=*/false,
+                      /*put_document_stats=*/nullptr),
+      IsOk());
+  EXPECT_THAT(
+      handler->Handle(tokenized_document1, document_id1,
+                      put_result1.old_document_id, /*recovery_mode=*/false,
+                      /*put_document_stats=*/nullptr),
+      IsOk());
   EXPECT_THAT(index->last_added_document_id(), Eq(document_id1));
   EXPECT_THAT(index->LiteIndexNeedSort(), IsFalse());
 
   // Handle doc2. The LiteIndex should have an unsorted portion after adding
-  EXPECT_THAT(handler->Handle(tokenized_document2, document_id2,
-                              /*recovery_mode=*/false,
-                              /*put_document_stats=*/nullptr),
-              IsOk());
+  EXPECT_THAT(
+      handler->Handle(tokenized_document2, document_id2,
+                      put_result2.old_document_id, /*recovery_mode=*/false,
+                      /*put_document_stats=*/nullptr),
+      IsOk());
   EXPECT_THAT(index->last_added_document_id(), Eq(document_id2));
 
   // Hits in the hit buffer:
@@ -580,18 +586,21 @@
           /*build_property_existence_metadata_hits=*/true));
 
   // Handle all docs
-  EXPECT_THAT(handler->Handle(tokenized_document0, document_id0,
-                              /*recovery_mode=*/false,
-                              /*put_document_stats=*/nullptr),
-              IsOk());
-  EXPECT_THAT(handler->Handle(tokenized_document1, document_id1,
-                              /*recovery_mode=*/false,
-                              /*put_document_stats=*/nullptr),
-              IsOk());
-  EXPECT_THAT(handler->Handle(tokenized_document2, document_id2,
-                              /*recovery_mode=*/false,
-                              /*put_document_stats=*/nullptr),
-              IsOk());
+  EXPECT_THAT(
+      handler->Handle(tokenized_document0, document_id0,
+                      put_result0.old_document_id, /*recovery_mode=*/false,
+                      /*put_document_stats=*/nullptr),
+      IsOk());
+  EXPECT_THAT(
+      handler->Handle(tokenized_document1, document_id1,
+                      put_result1.old_document_id, /*recovery_mode=*/false,
+                      /*put_document_stats=*/nullptr),
+      IsOk());
+  EXPECT_THAT(
+      handler->Handle(tokenized_document2, document_id2,
+                      put_result2.old_document_id, /*recovery_mode=*/false,
+                      /*put_document_stats=*/nullptr),
+      IsOk());
   EXPECT_THAT(index->last_added_document_id(), Eq(document_id2));
 
   // We've disabled sorting during indexing so the HitBuffer's unsorted section
diff --git a/icing/jni/icing-search-engine-jni.cc b/icing/jni/icing-search-engine-jni.cc
index afba2a1..de431f5 100644
--- a/icing/jni/icing-search-engine-jni.cc
+++ b/icing/jni/icing-search-engine-jni.cc
@@ -15,6 +15,7 @@
 #include <jni.h>
 
 #include <string>
+#include <string_view>
 #include <utility>
 
 #include "icing/icing-search-engine.h"
@@ -137,6 +138,23 @@
   return SerializeProtoToJniByteArray(env, get_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_nativeGetSchemaForDatabase(
+    JNIEnv* env, jclass clazz, jobject object, jstring database) {
+  icing::lib::IcingSearchEngine* icing =
+      GetIcingSearchEnginePointer(env, object);
+
+  icing::lib::ScopedUtfChars scoped_database_chars(env, database);
+  std::string_view database_str_view(scoped_database_chars.c_str(),
+                                     scoped_database_chars.size());
+  icing::lib::GetSchemaResultProto get_schema_result_proto =
+      icing->GetSchema(database_str_view);
+
+  return SerializeProtoToJniByteArray(env, get_schema_result_proto);
+}
+
 jbyteArray nativeGetSchemaType(JNIEnv* env, jclass clazz, jobject object,
                                jstring schema_type) {
   icing::lib::IcingSearchEngine* icing =
@@ -249,8 +267,9 @@
   return;
 }
 
-jbyteArray nativeOpenWriteBlob(
-    JNIEnv* env, jclass clazz, jobject object, jbyteArray blob_handle_bytes) {
+// TODO(b/273591938): Change this API back to the pre-registered API.
+jbyteArray nativeOpenWriteBlob(JNIEnv* env, jclass clazz, jobject object,
+                               jbyteArray blob_handle_bytes) {
   icing::lib::IcingSearchEngine* icing =
       GetIcingSearchEnginePointer(env, object);
 
@@ -266,8 +285,25 @@
   return SerializeProtoToJniByteArray(env, blob_result_proto);
 }
 
-jbyteArray nativeOpenReadBlob(
-    JNIEnv* env, jclass clazz, jobject object, jbyteArray blob_handle_bytes) {
+jbyteArray nativeRemoveBlob(JNIEnv* env, jclass clazz, jobject object,
+                            jbyteArray blob_handle_bytes) {
+  icing::lib::IcingSearchEngine* icing =
+      GetIcingSearchEnginePointer(env, object);
+
+  icing::lib::PropertyProto::BlobHandleProto blob_handle;
+  if (!ParseProtoFromJniByteArray(env, blob_handle_bytes, &blob_handle)) {
+    ICING_LOG(icing::lib::ERROR)
+        << "Failed to parse BlobHandle in nativeRemoveBlob";
+    return nullptr;
+  }
+
+  icing::lib::BlobProto blob_result_proto = icing->RemoveBlob(blob_handle);
+
+  return SerializeProtoToJniByteArray(env, blob_result_proto);
+}
+
+jbyteArray nativeOpenReadBlob(JNIEnv* env, jclass clazz, jobject object,
+                              jbyteArray blob_handle_bytes) {
   icing::lib::IcingSearchEngine* icing =
       GetIcingSearchEnginePointer(env, object);
 
@@ -283,8 +319,8 @@
   return SerializeProtoToJniByteArray(env, blob_result_proto);
 }
 
-jbyteArray nativeCommitBlob(
-    JNIEnv* env, jclass clazz, jobject object, jbyteArray blob_handle_bytes) {
+jbyteArray nativeCommitBlob(JNIEnv* env, jclass clazz, jobject object,
+                            jbyteArray blob_handle_bytes) {
   icing::lib::IcingSearchEngine* icing =
       GetIcingSearchEnginePointer(env, object);
 
@@ -578,6 +614,9 @@
       {"nativeOpenWriteBlob",
        "(Lcom/google/android/icing/IcingSearchEngineImpl;[B)[B",
        reinterpret_cast<void*>(nativeOpenWriteBlob)},
+      {"nativeRemoveBlob",
+       "(Lcom/google/android/icing/IcingSearchEngineImpl;[B)[B",
+       reinterpret_cast<void*>(nativeRemoveBlob)},
       {"nativeOpenReadBlob",
        "(Lcom/google/android/icing/IcingSearchEngineImpl;[B)[B",
        reinterpret_cast<void*>(nativeOpenReadBlob)},
diff --git a/icing/join/doc-join-info.cc b/icing/join/document-join-id-pair.cc
similarity index 83%
rename from icing/join/doc-join-info.cc
rename to icing/join/document-join-id-pair.cc
index 3b06f01..2bd9141 100644
--- a/icing/join/doc-join-info.cc
+++ b/icing/join/document-join-id-pair.cc
@@ -12,9 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "icing/join/doc-join-info.h"
-
-#include <cstdint>
+#include "icing/join/document-join-id-pair.h"
 
 #include "icing/schema/joinable-property.h"
 #include "icing/store/document-id.h"
@@ -23,8 +21,8 @@
 namespace icing {
 namespace lib {
 
-DocJoinInfo::DocJoinInfo(DocumentId document_id,
-                         JoinablePropertyId joinable_property_id) {
+DocumentJoinIdPair::DocumentJoinIdPair(
+    DocumentId document_id, JoinablePropertyId joinable_property_id) {
   Value temp_value = 0;
   bit_util::BitfieldSet(/*new_value=*/document_id,
                         /*lsb_offset=*/kJoinablePropertyIdBits,
@@ -35,12 +33,12 @@
   value_ = temp_value;
 }
 
-DocumentId DocJoinInfo::document_id() const {
+DocumentId DocumentJoinIdPair::document_id() const {
   return bit_util::BitfieldGet(value_, /*lsb_offset=*/kJoinablePropertyIdBits,
                                /*len=*/kDocumentIdBits);
 }
 
-JoinablePropertyId DocJoinInfo::joinable_property_id() const {
+JoinablePropertyId DocumentJoinIdPair::joinable_property_id() const {
   return bit_util::BitfieldGet(value_, /*lsb_offset=*/0,
                                /*len=*/kJoinablePropertyIdBits);
 }
diff --git a/icing/join/doc-join-info.h b/icing/join/document-join-id-pair.h
similarity index 62%
rename from icing/join/doc-join-info.h
rename to icing/join/document-join-id-pair.h
index 7696b92..603b1fc 100644
--- a/icing/join/doc-join-info.h
+++ b/icing/join/document-join-id-pair.h
@@ -12,10 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef ICING_JOIN_DOC_JOIN_INFO
-#define ICING_JOIN_DOC_JOIN_INFO
+#ifndef ICING_JOIN_DOCUMENT_JOIN_ID_PAIR_H_
+#define ICING_JOIN_DOCUMENT_JOIN_ID_PAIR_H_
 
+#include <cstddef>
 #include <cstdint>
+#include <functional>
 #include <limits>
 
 #include "icing/schema/joinable-property.h"
@@ -24,16 +26,23 @@
 namespace icing {
 namespace lib {
 
-// DocJoinInfo is composed of document_id and joinable_property_id.
-class DocJoinInfo {
+// DocumentJoinIdPair is composed of document_id and joinable_property_id.
+class DocumentJoinIdPair {
  public:
-  // The datatype used to encode DocJoinInfo information: the document_id and
-  // joinable_property_id.
+  // The datatype used to encode DocumentJoinIdPair information: the document_id
+  // and joinable_property_id.
   using Value = uint32_t;
 
+  struct Hasher {
+    std::size_t operator()(
+        const DocumentJoinIdPair& document_join_id_pair) const {
+      return std::hash<Value>()(document_join_id_pair.value());
+    }
+  };
+
   static_assert(kDocumentIdBits + kJoinablePropertyIdBits <= sizeof(Value) * 8,
                 "Cannot encode document id and joinable property id in "
-                "DocJoinInfo::Value");
+                "DocumentJoinIdPair::Value");
 
   // All bits of kInvalidValue are 1, and it contains:
   // - 0b1 for 4 unused bits.
@@ -44,10 +53,17 @@
   //   it doesn't matter what JoinablePropertyId we set for kInvalidValue.
   static constexpr Value kInvalidValue = std::numeric_limits<Value>::max();
 
-  explicit DocJoinInfo(DocumentId document_id,
-                       JoinablePropertyId joinable_property_id);
+  // Default constexpr constructor to construct an invalid DocumentJoinIdPair.
+  constexpr DocumentJoinIdPair() : value_(kInvalidValue) {}
 
-  explicit DocJoinInfo(Value value = kInvalidValue) : value_(value) {}
+  explicit DocumentJoinIdPair(DocumentId document_id,
+                              JoinablePropertyId joinable_property_id);
+
+  explicit DocumentJoinIdPair(Value value) : value_(value) {}
+
+  bool operator==(const DocumentJoinIdPair& other) const {
+    return value_ == other.value_;
+  }
 
   bool is_valid() const { return value_ != kInvalidValue; }
   Value value() const { return value_; }
@@ -58,9 +74,9 @@
   // Value bits layout: 4 unused + 22 document_id + 6 joinable_property_id.
   Value value_;
 } __attribute__((packed));
-static_assert(sizeof(DocJoinInfo) == 4, "");
+static_assert(sizeof(DocumentJoinIdPair) == 4, "");
 
 }  // namespace lib
 }  // namespace icing
 
-#endif  // ICING_JOIN_DOC_JOIN_INFO
+#endif  // ICING_JOIN_DOCUMENT_JOIN_ID_PAIR_H_
diff --git a/icing/join/doc-join-info_test.cc b/icing/join/document-join-id-pair_test.cc
similarity index 72%
rename from icing/join/doc-join-info_test.cc
rename to icing/join/document-join-id-pair_test.cc
index 7025473..2f76c88 100644
--- a/icing/join/doc-join-info_test.cc
+++ b/icing/join/document-join-id-pair_test.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "icing/join/doc-join-info.h"
+#include "icing/join/document-join-id-pair.h"
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
@@ -31,59 +31,62 @@
 static constexpr DocumentId kSomeDocumentId = 24;
 static constexpr JoinablePropertyId kSomeJoinablePropertyId = 5;
 
-TEST(DocJoinInfoTest, Accessors) {
-  DocJoinInfo doc_join_info(kSomeDocumentId, kSomeJoinablePropertyId);
+TEST(DocumentJoinIdPairTest, Accessors) {
+  DocumentJoinIdPair doc_join_info(kSomeDocumentId, kSomeJoinablePropertyId);
   EXPECT_THAT(doc_join_info.document_id(), Eq(kSomeDocumentId));
   EXPECT_THAT(doc_join_info.joinable_property_id(),
               Eq(kSomeJoinablePropertyId));
 }
 
-TEST(DocJoinInfoTest, Invalid) {
-  DocJoinInfo default_invalid;
+TEST(DocumentJoinIdPairTest, Invalid) {
+  DocumentJoinIdPair default_invalid;
   EXPECT_THAT(default_invalid.is_valid(), IsFalse());
 
-  // Also make sure the invalid DocJoinInfo contains an invalid document id.
+  // Also make sure the invalid DocumentJoinIdPair contains an invalid document
+  // id.
   EXPECT_THAT(default_invalid.document_id(), Eq(kInvalidDocumentId));
   EXPECT_THAT(default_invalid.joinable_property_id(),
               Eq(kMaxJoinablePropertyId));
 }
 
-TEST(DocJoinInfoTest, Valid) {
-  DocJoinInfo maximum_document_id_info(kMaxDocumentId, kSomeJoinablePropertyId);
+TEST(DocumentJoinIdPairTest, Valid) {
+  DocumentJoinIdPair maximum_document_id_info(kMaxDocumentId,
+                                              kSomeJoinablePropertyId);
   EXPECT_THAT(maximum_document_id_info.is_valid(), IsTrue());
   EXPECT_THAT(maximum_document_id_info.document_id(), Eq(kMaxDocumentId));
   EXPECT_THAT(maximum_document_id_info.joinable_property_id(),
               Eq(kSomeJoinablePropertyId));
 
-  DocJoinInfo maximum_joinable_property_id_info(kSomeDocumentId,
-                                                kMaxJoinablePropertyId);
+  DocumentJoinIdPair maximum_joinable_property_id_info(kSomeDocumentId,
+                                                       kMaxJoinablePropertyId);
   EXPECT_THAT(maximum_joinable_property_id_info.is_valid(), IsTrue());
   EXPECT_THAT(maximum_joinable_property_id_info.document_id(),
               Eq(kSomeDocumentId));
   EXPECT_THAT(maximum_joinable_property_id_info.joinable_property_id(),
               Eq(kMaxJoinablePropertyId));
 
-  DocJoinInfo minimum_document_id_info(kMinDocumentId, kSomeJoinablePropertyId);
+  DocumentJoinIdPair minimum_document_id_info(kMinDocumentId,
+                                              kSomeJoinablePropertyId);
   EXPECT_THAT(minimum_document_id_info.is_valid(), IsTrue());
   EXPECT_THAT(minimum_document_id_info.document_id(), Eq(kMinDocumentId));
   EXPECT_THAT(minimum_document_id_info.joinable_property_id(),
               Eq(kSomeJoinablePropertyId));
 
-  DocJoinInfo minimum_joinable_property_id_info(kSomeDocumentId,
-                                                kMinJoinablePropertyId);
+  DocumentJoinIdPair minimum_joinable_property_id_info(kSomeDocumentId,
+                                                       kMinJoinablePropertyId);
   EXPECT_THAT(minimum_joinable_property_id_info.is_valid(), IsTrue());
   EXPECT_THAT(minimum_joinable_property_id_info.document_id(),
               Eq(kSomeDocumentId));
   EXPECT_THAT(minimum_joinable_property_id_info.joinable_property_id(),
               Eq(kMinJoinablePropertyId));
 
-  DocJoinInfo all_maximum_info(kMaxDocumentId, kMaxJoinablePropertyId);
+  DocumentJoinIdPair all_maximum_info(kMaxDocumentId, kMaxJoinablePropertyId);
   EXPECT_THAT(all_maximum_info.is_valid(), IsTrue());
   EXPECT_THAT(all_maximum_info.document_id(), Eq(kMaxDocumentId));
   EXPECT_THAT(all_maximum_info.joinable_property_id(),
               Eq(kMaxJoinablePropertyId));
 
-  DocJoinInfo all_minimum_info(kMinDocumentId, kMinJoinablePropertyId);
+  DocumentJoinIdPair all_minimum_info(kMinDocumentId, kMinJoinablePropertyId);
   EXPECT_THAT(all_minimum_info.is_valid(), IsTrue());
   EXPECT_THAT(all_minimum_info.document_id(), Eq(kMinDocumentId));
   EXPECT_THAT(all_minimum_info.joinable_property_id(),
diff --git a/icing/join/join-children-fetcher-impl-deprecated.cc b/icing/join/join-children-fetcher-impl-deprecated.cc
new file mode 100644
index 0000000..34391d5
--- /dev/null
+++ b/icing/join/join-children-fetcher-impl-deprecated.cc
@@ -0,0 +1,59 @@
+// Copyright (C) 2023 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/join-children-fetcher-impl-deprecated.h"
+
+#include <memory>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#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/scoring/scored-document-hit.h"
+#include "icing/store/document-id.h"
+
+namespace icing {
+namespace lib {
+
+/* static */ libtextclassifier3::StatusOr<
+    std::unique_ptr<JoinChildrenFetcherImplDeprecated>>
+JoinChildrenFetcherImplDeprecated::Create(
+    const JoinSpecProto& join_spec,
+    std::unordered_map<DocumentId, std::vector<ScoredDocumentHit>>&&
+        map_joinable_qualified_id) {
+  // TODO(b/256022027): So far we only support kQualifiedIdExpr for
+  // parent_property_expression, we could support more.
+  if (join_spec.parent_property_expression() != kQualifiedIdExpr) {
+    return absl_ports::UnimplementedError(absl_ports::StrCat(
+        "Parent property expression must be ", kQualifiedIdExpr));
+  }
+
+  return std::unique_ptr<JoinChildrenFetcherImplDeprecated>(
+      new JoinChildrenFetcherImplDeprecated(
+          join_spec, std::move(map_joinable_qualified_id)));
+}
+
+libtextclassifier3::StatusOr<std::vector<ScoredDocumentHit>>
+JoinChildrenFetcherImplDeprecated::GetChildren(DocumentId parent_doc_id) const {
+  if (auto iter = map_joinable_qualified_id_.find(parent_doc_id);
+      iter != map_joinable_qualified_id_.end()) {
+    return iter->second;
+  }
+  return std::vector<ScoredDocumentHit>();
+}
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/join/join-children-fetcher-impl-deprecated.h b/icing/join/join-children-fetcher-impl-deprecated.h
new file mode 100644
index 0000000..a80dbc7
--- /dev/null
+++ b/icing/join/join-children-fetcher-impl-deprecated.h
@@ -0,0 +1,76 @@
+// Copyright (C) 2023 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_JOIN_CHILDREN_FETCHER_IMPL_DEPRECATED_H_
+#define ICING_JOIN_JOIN_CHILDREN_FETCHER_IMPL_DEPRECATED_H_
+
+#include <memory>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/join/join-children-fetcher.h"
+#include "icing/proto/search.pb.h"
+#include "icing/scoring/scored-document-hit.h"
+#include "icing/store/document-id.h"
+
+namespace icing {
+namespace lib {
+
+// A class that provides the GetChildren method for joins to fetch all children
+// documents given a parent document id. Only QualifiedIdJoinIndexImplV1 and
+// QualifiedIdJoinIndexImplV2 will use this class, since we can construct the
+// map without parent document id available.
+//
+// Internally, the class maintains a map for each joinable value type that
+// groups children according to the joinable values. Currently we only support
+// QUALIFIED_ID joining, in which the joinable value type is document id.
+class JoinChildrenFetcherImplDeprecated : public JoinChildrenFetcher {
+ public:
+  // Creates JoinChildrenFetcherImplDeprecated.
+  //
+  // Returns:
+  //   - A JoinChildrenFetcherImplDeprecated instance on success.
+  //   - UNIMPLEMENTED_ERROR if the join type specified by join_spec is not
+  //     supported.
+  static libtextclassifier3::StatusOr<
+      std::unique_ptr<JoinChildrenFetcherImplDeprecated>>
+  Create(const JoinSpecProto& join_spec,
+         std::unordered_map<DocumentId, std::vector<ScoredDocumentHit>>&&
+             map_joinable_qualified_id);
+
+  ~JoinChildrenFetcherImplDeprecated() override = default;
+
+  libtextclassifier3::StatusOr<std::vector<ScoredDocumentHit>> GetChildren(
+      DocumentId parent_doc_id) const override;
+
+ private:
+  explicit JoinChildrenFetcherImplDeprecated(
+      const JoinSpecProto& join_spec,
+      std::unordered_map<DocumentId, std::vector<ScoredDocumentHit>>&&
+          map_joinable_qualified_id)
+      : JoinChildrenFetcher(join_spec),
+        map_joinable_qualified_id_(std::move(map_joinable_qualified_id)) {}
+
+  // The map that groups children by qualified id used to support QualifiedId
+  // joining. The joining type is document id.
+  std::unordered_map<DocumentId, std::vector<ScoredDocumentHit>>
+      map_joinable_qualified_id_;
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_JOIN_JOIN_CHILDREN_FETCHER_IMPL_DEPRECATED_H_
diff --git a/icing/join/join-children-fetcher_test.cc b/icing/join/join-children-fetcher-impl-deprecated_test.cc
similarity index 67%
rename from icing/join/join-children-fetcher_test.cc
rename to icing/join/join-children-fetcher-impl-deprecated_test.cc
index 92a7a81..0c40531 100644
--- a/icing/join/join-children-fetcher_test.cc
+++ b/icing/join/join-children-fetcher-impl-deprecated_test.cc
@@ -12,15 +12,22 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "icing/join/join-children-fetcher.h"
+#include "icing/join/join-children-fetcher-impl-deprecated.h"
 
+#include <memory>
+#include <string>
 #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/join/join-processor.h"
 #include "icing/proto/search.pb.h"
 #include "icing/schema/section.h"
+#include "icing/scoring/scored-document-hit.h"
+#include "icing/store/document-id.h"
 #include "icing/testing/common-matchers.h"
 
 namespace icing {
@@ -31,7 +38,7 @@
 using ::testing::ElementsAre;
 using ::testing::IsEmpty;
 
-TEST(JoinChildrenFetcherTest, FetchQualifiedIdJoinChildren) {
+TEST(JoinChildrenFetcherImplDeprecatedTest, FetchQualifiedIdJoinChildren) {
   JoinSpecProto join_spec;
   join_spec.set_parent_property_expression(
       std::string(JoinProcessor::kQualifiedIdExpr));
@@ -47,14 +54,17 @@
   map_joinable_qualified_id[parent_doc_id].push_back(child1);
   map_joinable_qualified_id[parent_doc_id].push_back(child2);
 
-  JoinChildrenFetcher fetcher(join_spec, std::move(map_joinable_qualified_id));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<JoinChildrenFetcherImplDeprecated> fetcher,
+      JoinChildrenFetcherImplDeprecated::Create(
+          join_spec, std::move(map_joinable_qualified_id)));
   ICING_ASSERT_OK_AND_ASSIGN(std::vector<ScoredDocumentHit> children,
-                             fetcher.GetChildren(parent_doc_id));
+                             fetcher->GetChildren(parent_doc_id));
   EXPECT_THAT(children, ElementsAre(EqualsScoredDocumentHit(child1),
                                     EqualsScoredDocumentHit(child2)));
 }
 
-TEST(JoinChildrenFetcherTest, FetchJoinEmptyChildren) {
+TEST(JoinChildrenFetcherImplDeprecatedTest, FetchJoinEmptyChildren) {
   JoinSpecProto join_spec;
   join_spec.set_parent_property_expression(
       std::string(JoinProcessor::kQualifiedIdExpr));
@@ -62,18 +72,23 @@
 
   DocumentId parent_doc_id = 0;
 
-  JoinChildrenFetcher fetcher(join_spec, /*map_joinable_qualified_id=*/{});
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<JoinChildrenFetcherImplDeprecated> fetcher,
+      JoinChildrenFetcherImplDeprecated::Create(
+          join_spec,
+          /*map_joinable_qualified_id=*/{}));
   ICING_ASSERT_OK_AND_ASSIGN(std::vector<ScoredDocumentHit> children,
-                             fetcher.GetChildren(parent_doc_id));
+                             fetcher->GetChildren(parent_doc_id));
   EXPECT_THAT(children, IsEmpty());
 }
 
-TEST(JoinChildrenFetcherTest, UnsupportedJoin) {
+TEST(JoinChildrenFetcherImplDeprecatedTest, UnsupportedJoin) {
   JoinSpecProto join_spec;
   join_spec.set_parent_property_expression("name");
   join_spec.set_child_property_expression("sender");
-  JoinChildrenFetcher fetcher(join_spec, /*map_joinable_qualified_id=*/{});
-  EXPECT_THAT(fetcher.GetChildren(0),
+
+  EXPECT_THAT(JoinChildrenFetcherImplDeprecated::Create(
+                  join_spec, /*map_joinable_qualified_id=*/{}),
               StatusIs(libtextclassifier3::StatusCode::UNIMPLEMENTED));
 }
 
diff --git a/icing/join/join-children-fetcher-impl-v3.cc b/icing/join/join-children-fetcher-impl-v3.cc
new file mode 100644
index 0000000..456c3ba
--- /dev/null
+++ b/icing/join/join-children-fetcher-impl-v3.cc
@@ -0,0 +1,166 @@
+// Copyright (C) 2024 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/join-children-fetcher-impl-v3.h"
+
+#include <algorithm>
+#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/canonical_errors.h"
+#include "icing/absl_ports/str_cat.h"
+#include "icing/join/document-join-id-pair.h"
+#include "icing/join/qualified-id-join-index.h"
+#include "icing/proto/scoring.pb.h"
+#include "icing/proto/search.pb.h"
+#include "icing/schema/joinable-property.h"
+#include "icing/schema/schema-store.h"
+#include "icing/scoring/scored-document-hit.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<JoinChildrenFetcherImplV3>>
+JoinChildrenFetcherImplV3::Create(
+    const JoinSpecProto& join_spec, const SchemaStore* schema_store,
+    const DocumentStore* doc_store,
+    const QualifiedIdJoinIndex* qualified_id_join_index,
+    int64_t current_time_ms,
+    std::vector<ScoredDocumentHit>&& child_scored_document_hits) {
+  ICING_RETURN_ERROR_IF_NULL(schema_store);
+  ICING_RETURN_ERROR_IF_NULL(doc_store);
+  ICING_RETURN_ERROR_IF_NULL(qualified_id_join_index);
+
+  // TODO(b/256022027): So far we only support kQualifiedIdExpr for
+  // parent_property_expression, we could support more.
+  if (join_spec.parent_property_expression() != kQualifiedIdExpr) {
+    return absl_ports::UnimplementedError(absl_ports::StrCat(
+        "Parent property expression must be ", kQualifiedIdExpr));
+  }
+
+  if (qualified_id_join_index->version() !=
+      QualifiedIdJoinIndex::Version::kV3) {
+    return absl_ports::InvalidArgumentError(
+        "QualifiedIdJoinIndex version must be v3 with "
+        "JoinChildrenFetcherImplV3.");
+  }
+
+  // For each child scored document hit, we need to find the joinable property
+  // id that matches the joinable property specified in the join spec.
+  std::unordered_map<DocumentJoinIdPair, ScoredDocumentHit,
+                     DocumentJoinIdPair::Hasher>
+      child_join_id_pair_to_scored_document_hit_map;
+  for (ScoredDocumentHit& child_scored_document_hit :
+       child_scored_document_hits) {
+    DocumentId child_doc_id = child_scored_document_hit.document_id();
+
+    // Get the document filter data for the child document.
+    std::optional<DocumentFilterData> child_doc_filter_data =
+        doc_store->GetAliveDocumentFilterData(child_doc_id, current_time_ms);
+    if (!child_doc_filter_data) {
+      continue;
+    }
+
+    // Get the joinable property metadata.
+    ICING_ASSIGN_OR_RETURN(const JoinablePropertyMetadata* metadata,
+                           schema_store->GetJoinablePropertyMetadata(
+                               child_doc_filter_data->schema_type_id(),
+                               join_spec.child_property_expression()));
+    if (metadata == nullptr) {
+      continue;
+    }
+
+    child_join_id_pair_to_scored_document_hit_map.insert(
+        {DocumentJoinIdPair(child_doc_id, metadata->id),
+         std::move(child_scored_document_hit)});
+  }
+
+  return std::unique_ptr<JoinChildrenFetcherImplV3>(
+      new JoinChildrenFetcherImplV3(
+          join_spec, qualified_id_join_index,
+          std::move(child_join_id_pair_to_scored_document_hit_map)));
+}
+
+libtextclassifier3::StatusOr<std::vector<ScoredDocumentHit>>
+JoinChildrenFetcherImplV3::GetChildren(DocumentId parent_doc_id) const {
+  // If the parent_doc_id and its children are already in the cache, return the
+  // cached value.
+  if (auto cache_itr = cached_parent_to_children_map_.find(parent_doc_id);
+      cache_itr != cached_parent_to_children_map_.end()) {
+    return cache_itr->second;
+  }
+
+  // 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));
+
+  // Filter and construct child_scored_document_hits for the given parent doc
+  // id.
+  // - child_join_id_pair_to_scored_document_hit_map_ contains (DocumentId,
+  //   JoinablePropertyId) of the child documents in the child result set and
+  //   the desired joinable property id which matches the joinable property
+  //   specified in the join spec.
+  // - The DocumentJoinIdPair list fetched from the join index may contain child
+  //   documents that are not in the child result set, or the joinable property
+  //   id is not the desired one. For example:
+  //   - Parent document 1 may have 4 child DocumentJoinIdPairs:
+  //     - (doc_id=100, joinable_property_id=1)
+  //     - (doc_id=100, joinable_property_id=2)
+  //     - (doc_id=101, joinable_property_id=1)
+  //     - (doc_id=102, joinable_property_id=1)
+  //   - All of these 4 pairs will be fetched from the join index.
+  //   - Now, the child query obtains result doc ids = [100, 102, 103], and the
+  //     client wants to join the child document by the joinable property "foo",
+  //     which has joinable property id 1 in its schema.
+  //   - In this case, when handling parent document 1, we should filter out
+  //     children [(100, 2), (101, 1)] since they are not in the child result
+  //     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) {
+    if (auto filter_itr = child_join_id_pair_to_scored_document_hit_map_.find(
+            child_document_join_id_pair);
+        filter_itr != child_join_id_pair_to_scored_document_hit_map_.end()) {
+      child_scored_document_hits.push_back(filter_itr->second);
+    }
+  }
+
+  // Sort child_scored_document_hits according to the child scoring spec.
+  ScoredDocumentHitComparator score_comparator(
+      /*is_descending=*/join_spec_.nested_spec().scoring_spec().order_by() ==
+      ScoringSpecProto::Order::DESC);
+  std::sort(child_scored_document_hits.begin(),
+            child_scored_document_hits.end(), score_comparator);
+
+  cached_parent_to_children_map_.insert(
+      {parent_doc_id, child_scored_document_hits});
+
+  return child_scored_document_hits;
+}
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/join/join-children-fetcher-impl-v3.h b/icing/join/join-children-fetcher-impl-v3.h
new file mode 100644
index 0000000..37afbc4
--- /dev/null
+++ b/icing/join/join-children-fetcher-impl-v3.h
@@ -0,0 +1,97 @@
+// Copyright (C) 2024 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_JOIN_CHILDREN_FETCHER_IMPL_V3_H_
+#define ICING_JOIN_JOIN_CHILDREN_FETCHER_IMPL_V3_H_
+
+#include <cstdint>
+#include <memory>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/join/document-join-id-pair.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 {
+namespace lib {
+
+// A class that provides the GetChildren method for joins to fetch all children
+// documents given a parent document id.
+// - It does lazy lookup and caches the results after lookup and filter.
+// - Only QualifiedIdJoinIndexImplV3 will use this class.
+class JoinChildrenFetcherImplV3 : public JoinChildrenFetcher {
+ public:
+  // Creates JoinChildrenFetcherImplV3.
+  //
+  // Returns:
+  //   - A JoinChildrenFetcherImplV3 instance on success.
+  //   - FAILED_PRECONDITION_ERROR if any of the input pointer is null
+  //   - UNIMPLEMENTED_ERROR if the join type specified by join_spec is not
+  //     supported.
+  //   - INVALID_ARGUMENT_ERROR if qualified_id_join_index is not v3.
+  static libtextclassifier3::StatusOr<
+      std::unique_ptr<JoinChildrenFetcherImplV3>>
+  Create(const JoinSpecProto& join_spec, const SchemaStore* schema_store,
+         const DocumentStore* doc_store,
+         const QualifiedIdJoinIndex* qualified_id_join_index,
+         int64_t current_time_ms,
+         std::vector<ScoredDocumentHit>&& child_scored_document_hits);
+
+  ~JoinChildrenFetcherImplV3() override = default;
+
+  libtextclassifier3::StatusOr<std::vector<ScoredDocumentHit>> GetChildren(
+      DocumentId parent_doc_id) const override;
+
+ private:
+  explicit JoinChildrenFetcherImplV3(
+      const JoinSpecProto& join_spec,
+      const QualifiedIdJoinIndex* qualified_id_join_index,
+      std::unordered_map<DocumentJoinIdPair, ScoredDocumentHit,
+                         DocumentJoinIdPair::Hasher>&&
+          child_join_id_pair_to_scored_document_hit_map)
+      : JoinChildrenFetcher(join_spec),
+        qualified_id_join_index_(*qualified_id_join_index),
+        child_join_id_pair_to_scored_document_hit_map_(
+            std::move(child_join_id_pair_to_scored_document_hit_map)) {}
+
+  const QualifiedIdJoinIndex& qualified_id_join_index_;  // Does not own.
+
+  // Map the child join id pair to its scored document hit. It will be used to
+  // filter the child join id pairs. See GetChildren() implementation for more
+  // details.
+  std::unordered_map<DocumentJoinIdPair, ScoredDocumentHit,
+                     DocumentJoinIdPair::Hasher>
+      child_join_id_pair_to_scored_document_hit_map_;
+
+  // The cache for storing parent document id to its children's scored document
+  // hits. GetChildren() will first check the cache to see if the parent
+  // document id is already in the cache. If not, it will do a lookup and
+  // filter the children (via child_join_id_pair_to_scored_document_hit_map_)
+  // and then store the result in the cache.
+  mutable std::unordered_map<DocumentId, std::vector<ScoredDocumentHit>>
+      cached_parent_to_children_map_;
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_JOIN_JOIN_CHILDREN_FETCHER_IMPL_V3_H_
diff --git a/icing/join/join-children-fetcher-impl-v3_test.cc b/icing/join/join-children-fetcher-impl-v3_test.cc
new file mode 100644
index 0000000..e6e3251
--- /dev/null
+++ b/icing/join/join-children-fetcher-impl-v3_test.cc
@@ -0,0 +1,918 @@
+// Copyright (C) 2024 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/join-children-fetcher-impl-v3.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/join/document-join-id-pair.h"
+#include "icing/join/join-processor.h"
+#include "icing/join/qualified-id-join-index-impl-v1.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/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/schema/section.h"
+#include "icing/scoring/scored-document-hit.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::IsEmpty;
+using ::testing::IsTrue;
+
+class JoinChildrenFetcherImplV3Test : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+    base_dir_ = GetTestTempDir() + "/icing_test";
+    ASSERT_THAT(filesystem_.CreateDirectoryRecursively(base_dir_.c_str()),
+                IsTrue());
+
+    schema_store_dir_ = base_dir_ + "/schema_store";
+    doc_store_dir_ = base_dir_ + "/doc_store";
+    qualified_id_join_index_dir_ = base_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("Message")
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("sender")
+                                     .SetDataTypeJoinableString(
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                     .SetCardinality(CARDINALITY_OPTIONAL))
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("receiver")
+                                     .SetDataTypeJoinableString(
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                     .SetCardinality(CARDINALITY_OPTIONAL)))
+            .AddType(SchemaTypeConfigBuilder().SetType("Email").AddProperty(
+                PropertyConfigBuilder()
+                    .SetName("sender")
+                    .SetDataTypeJoinableString(JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                    .SetCardinality(CARDINALITY_OPTIONAL)))
+
+            .Build();
+    ASSERT_THAT(schema_store_->SetSchema(
+                    schema, /*ignore_errors_and_delete_documents=*/false,
+                    /*allow_circular_schema_definitions=*/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,
+                              /*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(base_dir_.c_str());
+  }
+
+  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));
+
+    ICING_ASSIGN_OR_RETURN(
+        std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
+        QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
+                                               qualified_id_join_index_.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_;
+  FakeClock fake_clock_;
+
+  std::string base_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_;
+};
+
+TEST_F(JoinChildrenFetcherImplV3Test, CreationWithNullPointerShouldFail) {
+  JoinSpecProto join_spec;
+  join_spec.set_parent_property_expression(
+      std::string(JoinProcessor::kQualifiedIdExpr));
+  join_spec.set_child_property_expression("sender");
+  join_spec.mutable_nested_spec()->mutable_scoring_spec()->set_order_by(
+      ScoringSpecProto::Order::ASC);
+
+  EXPECT_THAT(JoinChildrenFetcherImplV3::Create(
+                  join_spec, /*schema_store=*/nullptr, doc_store_.get(),
+                  qualified_id_join_index_.get(),
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+                  /*child_scored_document_hits=*/{}),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+
+  EXPECT_THAT(JoinChildrenFetcherImplV3::Create(
+                  join_spec, schema_store_.get(), /*doc_store=*/nullptr,
+                  qualified_id_join_index_.get(),
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+                  /*child_scored_document_hits=*/{}),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+
+  EXPECT_THAT(JoinChildrenFetcherImplV3::Create(
+                  join_spec, schema_store_.get(), doc_store_.get(),
+                  /*qualified_id_join_index=*/nullptr,
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+                  /*child_scored_document_hits=*/{}),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+}
+
+TEST_F(JoinChildrenFetcherImplV3Test,
+       CreationWithWrongVersionJoinIndexShouldFail) {
+  JoinSpecProto join_spec;
+  join_spec.set_parent_property_expression(
+      std::string(JoinProcessor::kQualifiedIdExpr));
+  join_spec.set_child_property_expression("sender");
+  join_spec.mutable_nested_spec()->mutable_scoring_spec()->set_order_by(
+      ScoringSpecProto::Order::ASC);
+
+  std::string qualified_id_join_index_dir_v1 =
+      qualified_id_join_index_dir_ + "_v1";
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<QualifiedIdJoinIndexImplV1> qualified_id_join_index_v1,
+      QualifiedIdJoinIndexImplV1::Create(
+          filesystem_, std::move(qualified_id_join_index_dir_v1),
+          /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true));
+
+  std::string qualified_id_join_index_dir_v2 =
+      qualified_id_join_index_dir_ + "_v2";
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<QualifiedIdJoinIndexImplV2> qualified_id_join_index_v2,
+      QualifiedIdJoinIndexImplV2::Create(
+          filesystem_, std::move(qualified_id_join_index_dir_v2),
+          /*pre_mapping_fbv=*/false));
+
+  EXPECT_THAT(JoinChildrenFetcherImplV3::Create(
+                  join_spec, schema_store_.get(), doc_store_.get(),
+                  qualified_id_join_index_v1.get(),
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+                  /*child_scored_document_hits=*/{}),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+
+  EXPECT_THAT(JoinChildrenFetcherImplV3::Create(
+                  join_spec, schema_store_.get(), doc_store_.get(),
+                  qualified_id_join_index_v2.get(),
+                  /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+                  /*child_scored_document_hits=*/{}),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+TEST_F(JoinChildrenFetcherImplV3Test, GetChildren) {
+  // Simulate the following scenario:
+  // - Email child schema has 1 joinable property: "sender" with joinable
+  //   property id 0.
+  //   - email1 joins with person1 by property "sender".
+  //   - email2 joins with person1 by property "sender".
+  //   - email3 joins with person2 by property "sender".
+  // - email1, email2 and email3 are in the child result set.
+  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();
+
+  DocumentProto email1 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email1")
+          .SetSchema("Email")
+          .AddStringProperty("sender", "pkg$db/namespace#person1")
+          .Build();
+  DocumentProto email2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email2")
+          .SetSchema("Email")
+          .AddStringProperty("sender", "pkg$db/namespace#person1")
+          .Build();
+  DocumentProto email3 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email3")
+          .SetSchema("Email")
+          .AddStringProperty("sender", "pkg$db/namespace#person2")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person1_id,
+                             PutAndIndexDocument(person1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person2_id,
+                             PutAndIndexDocument(person2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId email1_id, PutAndIndexDocument(email1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId email2_id, PutAndIndexDocument(email2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId email3_id, PutAndIndexDocument(email3));
+
+  ScoredDocumentHit scored_doc_hit_email1(email1_id, kSectionIdMaskNone,
+                                          /*score=*/1.0);
+  ScoredDocumentHit scored_doc_hit_email2(email2_id, kSectionIdMaskNone,
+                                          /*score=*/2.0);
+  ScoredDocumentHit scored_doc_hit_email3(email3_id, kSectionIdMaskNone,
+                                          /*score=*/3.0);
+
+  // Join by property "sender".
+  JoinSpecProto join_spec;
+  join_spec.set_parent_property_expression(
+      std::string(JoinProcessor::kQualifiedIdExpr));
+  join_spec.set_child_property_expression("sender");
+  join_spec.mutable_nested_spec()->mutable_scoring_spec()->set_order_by(
+      ScoringSpecProto::Order::ASC);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<JoinChildrenFetcherImplV3> fetcher,
+      JoinChildrenFetcherImplV3::Create(
+          join_spec, schema_store_.get(), doc_store_.get(),
+          qualified_id_join_index_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          /*child_scored_document_hits=*/
+          {scored_doc_hit_email1, scored_doc_hit_email2,
+           scored_doc_hit_email3}));
+
+  EXPECT_THAT(fetcher->GetChildren(person1_id),
+              IsOkAndHolds(
+                  ElementsAre(EqualsScoredDocumentHit(scored_doc_hit_email1),
+                              EqualsScoredDocumentHit(scored_doc_hit_email2))));
+  EXPECT_THAT(fetcher->GetChildren(person2_id),
+              IsOkAndHolds(
+                  ElementsAre(EqualsScoredDocumentHit(scored_doc_hit_email3))));
+}
+
+TEST_F(JoinChildrenFetcherImplV3Test,
+       GetChildrenJoinChildrenFromMultipleSchemas) {
+  // Simulate the following scenario:
+  // - Email child schema has 1 joinable property: "sender" with joinable
+  //   property id 0.
+  //   - email1 joins with person1 by property "sender".
+  //   - email2 joins with person2 by property "sender".
+  // - Message child schema has 2 joinable properties: "receiver", "sender" with
+  //   joinable property ids 0, 1 respectively.
+  //   - message1 joins with person2 by property "sender".
+  //   - message2 joins with person2 by property "sender".
+  //   - message3 joins with person1 by property "sender".
+  // - email1, email2, message1, message2 and message3 are in the child result
+  //   set.
+  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();
+
+  DocumentProto email1 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email1")
+          .SetSchema("Email")
+          .AddStringProperty("sender", "pkg$db/namespace#person1")
+          .Build();
+  DocumentProto email2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email2")
+          .SetSchema("Email")
+          .AddStringProperty("sender", "pkg$db/namespace#person2")
+          .Build();
+
+  DocumentProto message1 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "message1")
+          .SetSchema("Message")
+          .AddStringProperty("sender", "pkg$db/namespace#person2")
+          .Build();
+  DocumentProto message2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "message2")
+          .SetSchema("Message")
+          .AddStringProperty("sender", "pkg$db/namespace#person2")
+          .Build();
+  DocumentProto message3 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "message3")
+          .SetSchema("Message")
+          .AddStringProperty("sender", "pkg$db/namespace#person1")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person1_id,
+                             PutAndIndexDocument(person1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person2_id,
+                             PutAndIndexDocument(person2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId email1_id, PutAndIndexDocument(email1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId email2_id, PutAndIndexDocument(email2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message1_id,
+                             PutAndIndexDocument(message1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message2_id,
+                             PutAndIndexDocument(message2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message3_id,
+                             PutAndIndexDocument(message3));
+
+  ScoredDocumentHit scored_doc_hit_email1(email1_id, kSectionIdMaskNone,
+                                          /*score=*/1.0);
+  ScoredDocumentHit scored_doc_hit_email2(email2_id, kSectionIdMaskNone,
+                                          /*score=*/2.0);
+  ScoredDocumentHit scored_doc_hit_message1(message1_id, kSectionIdMaskNone,
+                                            /*score=*/3.0);
+  ScoredDocumentHit scored_doc_hit_message2(message2_id, kSectionIdMaskNone,
+                                            /*score=*/4.0);
+  ScoredDocumentHit scored_doc_hit_message3(message3_id, kSectionIdMaskNone,
+                                            /*score=*/5.0);
+
+  // Join by property "sender".
+  JoinSpecProto join_spec;
+  join_spec.set_parent_property_expression(
+      std::string(JoinProcessor::kQualifiedIdExpr));
+  join_spec.set_child_property_expression("sender");
+  join_spec.mutable_nested_spec()->mutable_scoring_spec()->set_order_by(
+      ScoringSpecProto::Order::ASC);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<JoinChildrenFetcherImplV3> fetcher,
+      JoinChildrenFetcherImplV3::Create(
+          join_spec, schema_store_.get(), doc_store_.get(),
+          qualified_id_join_index_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          /*child_scored_document_hits=*/
+          {scored_doc_hit_email1, scored_doc_hit_email2,
+           scored_doc_hit_message1, scored_doc_hit_message2,
+           scored_doc_hit_message3}));
+
+  EXPECT_THAT(fetcher->GetChildren(person1_id),
+              IsOkAndHolds(ElementsAre(
+                  EqualsScoredDocumentHit(scored_doc_hit_email1),
+                  EqualsScoredDocumentHit(scored_doc_hit_message3))));
+  EXPECT_THAT(fetcher->GetChildren(person2_id),
+              IsOkAndHolds(ElementsAre(
+                  EqualsScoredDocumentHit(scored_doc_hit_email2),
+                  EqualsScoredDocumentHit(scored_doc_hit_message1),
+                  EqualsScoredDocumentHit(scored_doc_hit_message2))));
+}
+
+TEST_F(JoinChildrenFetcherImplV3Test,
+       GetChildrenShouldFilterOutUndesiredJoinablePropertyIds) {
+  // Simulate the following scenario:
+  // - Message child schema has 2 joinable properties: "receiver", "sender" with
+  //   joinable property ids 0, 1 respectively.
+  //   - message1 joins with person1 by property "receiver".
+  //   - message2 joins with person1 by property "sender".
+  //   - message2 joins with person2 by property "receiver".
+  //   - message3 joins with person1 by property "receiver".
+  // - message1, message2 and message3 are in the child result set.
+  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();
+
+  DocumentProto message1 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "message1")
+          .SetSchema("Message")
+          .AddStringProperty("receiver", "pkg$db/namespace#person1")
+          .Build();
+  DocumentProto message2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "message2")
+          .SetSchema("Message")
+          .AddStringProperty("sender", "pkg$db/namespace#person1")
+          .AddStringProperty("receiver", "pkg$db/namespace#person2")
+          .Build();
+  DocumentProto message3 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "message3")
+          .SetSchema("Message")
+          .AddStringProperty("receiver", "pkg$db/namespace#person1")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person1_id,
+                             PutAndIndexDocument(person1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person2_id,
+                             PutAndIndexDocument(person2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message1_id,
+                             PutAndIndexDocument(message1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId message2_id,
+                             PutAndIndexDocument(message2));
+  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))));
+
+  ScoredDocumentHit scored_doc_hit_message1(message1_id, kSectionIdMaskNone,
+                                            /*score=*/1.0);
+  ScoredDocumentHit scored_doc_hit_message2(message2_id, kSectionIdMaskNone,
+                                            /*score=*/2.0);
+  ScoredDocumentHit scored_doc_hit_message3(message3_id, kSectionIdMaskNone,
+                                            /*score=*/3.0);
+
+  // Join by property "receiver".
+  JoinSpecProto join_spec;
+  join_spec.set_parent_property_expression(
+      std::string(JoinProcessor::kQualifiedIdExpr));
+  join_spec.set_child_property_expression("receiver");
+  join_spec.mutable_nested_spec()->mutable_scoring_spec()->set_order_by(
+      ScoringSpecProto::Order::ASC);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<JoinChildrenFetcherImplV3> fetcher,
+      JoinChildrenFetcherImplV3::Create(
+          join_spec, schema_store_.get(), doc_store_.get(),
+          qualified_id_join_index_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          /*child_scored_document_hits=*/
+          {scored_doc_hit_message1, scored_doc_hit_message2,
+           scored_doc_hit_message3}));
+
+  // Person1 should get message1 and message3, but not message2 since message2
+  // joins with person1 by property "sender", but join spec uses "receiver" to
+  // join.
+  EXPECT_THAT(fetcher->GetChildren(person1_id),
+              IsOkAndHolds(ElementsAre(
+                  EqualsScoredDocumentHit(scored_doc_hit_message1),
+                  EqualsScoredDocumentHit(scored_doc_hit_message3))));
+  // Person2 should get message2.
+  EXPECT_THAT(fetcher->GetChildren(person2_id),
+              IsOkAndHolds(ElementsAre(
+                  EqualsScoredDocumentHit(scored_doc_hit_message2))));
+}
+
+TEST_F(JoinChildrenFetcherImplV3Test,
+       GetChildrenShouldFilterOutUndesiredChildDocumentIds) {
+  // Simulate the following scenario:
+  // - Email child schema has 1 joinable property: "sender" with joinable
+  //   property id 0.
+  //   - email1 joins with person1 by property "sender".
+  //   - email2 joins with person1 by property "sender".
+  //   - email3 joins with person2 by property "sender".
+  //   - email4 joins with person3 by property "sender".
+  // - Only email1 and email4 are in the child result set.
+  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();
+  DocumentProto person3 = DocumentBuilder()
+                              .SetKey("pkg$db/namespace", "person3")
+                              .SetSchema("Person")
+                              .AddStringProperty("Name", "Eve")
+                              .Build();
+
+  DocumentProto email1 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email1")
+          .SetSchema("Email")
+          .AddStringProperty("sender", "pkg$db/namespace#person1")
+          .Build();
+  DocumentProto email2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email2")
+          .SetSchema("Email")
+          .AddStringProperty("sender", "pkg$db/namespace#person1")
+          .Build();
+  DocumentProto email3 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email3")
+          .SetSchema("Email")
+          .AddStringProperty("sender", "pkg$db/namespace#person2")
+          .Build();
+  DocumentProto email4 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email4")
+          .SetSchema("Email")
+          .AddStringProperty("sender", "pkg$db/namespace#person3")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person1_id,
+                             PutAndIndexDocument(person1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person2_id,
+                             PutAndIndexDocument(person2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person3_id,
+                             PutAndIndexDocument(person3));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId email1_id, PutAndIndexDocument(email1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId email2_id, PutAndIndexDocument(email2));
+  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))));
+
+  ScoredDocumentHit scored_doc_hit_email1(email1_id, kSectionIdMaskNone,
+                                          /*score=*/1.0);
+  ScoredDocumentHit scored_doc_hit_email4(email4_id, kSectionIdMaskNone,
+                                          /*score=*/4.0);
+
+  // Join by property "sender".
+  JoinSpecProto join_spec;
+  join_spec.set_parent_property_expression(
+      std::string(JoinProcessor::kQualifiedIdExpr));
+  join_spec.set_child_property_expression("sender");
+  join_spec.mutable_nested_spec()->mutable_scoring_spec()->set_order_by(
+      ScoringSpecProto::Order::ASC);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<JoinChildrenFetcherImplV3> fetcher,
+      JoinChildrenFetcherImplV3::Create(
+          join_spec, schema_store_.get(), doc_store_.get(),
+          qualified_id_join_index_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          /*child_scored_document_hits=*/
+          {scored_doc_hit_email1, scored_doc_hit_email4}));
+
+  // Person1 should get email1, but not email2 since email2 is not in the result
+  // set.
+  EXPECT_THAT(fetcher->GetChildren(person1_id),
+              IsOkAndHolds(
+                  ElementsAre(EqualsScoredDocumentHit(scored_doc_hit_email1))));
+  // Person2 should get empty result set since email3 is not in the result set.
+  EXPECT_THAT(fetcher->GetChildren(person2_id), IsOkAndHolds(IsEmpty()));
+  // Person3 should get email4.
+  EXPECT_THAT(fetcher->GetChildren(person3_id),
+              IsOkAndHolds(
+                  ElementsAre(EqualsScoredDocumentHit(scored_doc_hit_email4))));
+}
+
+TEST_F(JoinChildrenFetcherImplV3Test,
+       GetChildrenShouldSkipNonExistingJoinableProperty) {
+  // Simulate the following scenario:
+  // - Email child schema has 1 joinable property: "sender" with joinable
+  //   property id 0.
+  //   - email1 joins with person1 by property "sender".
+  //   - email2 joins with person1 by property "sender".
+  //   - email3 joins with person2 by property "sender".
+  // - email1, email2 and email3 are in the child result set, but email3 is
+  //   expired.
+  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();
+
+  DocumentProto email1 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email1")
+          .SetSchema("Email")
+          .AddStringProperty("sender", "pkg$db/namespace#person1")
+          .Build();
+  DocumentProto email2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email2")
+          .SetSchema("Email")
+          .AddStringProperty("sender", "pkg$db/namespace#person1")
+          .Build();
+  DocumentProto email3 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email3")
+          .SetSchema("Email")
+          .AddStringProperty("sender", "pkg$db/namespace#person2")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person1_id,
+                             PutAndIndexDocument(person1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person2_id,
+                             PutAndIndexDocument(person2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId email1_id, PutAndIndexDocument(email1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId email2_id, PutAndIndexDocument(email2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId email3_id, PutAndIndexDocument(email3));
+
+  ScoredDocumentHit scored_doc_hit_email1(email1_id, kSectionIdMaskNone,
+                                          /*score=*/1.0);
+  ScoredDocumentHit scored_doc_hit_email2(email2_id, kSectionIdMaskNone,
+                                          /*score=*/2.0);
+  ScoredDocumentHit scored_doc_hit_email3(email3_id, kSectionIdMaskNone,
+                                          /*score=*/3.0);
+
+  // Join by a non-existing property "foo".
+  JoinSpecProto join_spec;
+  join_spec.set_parent_property_expression(
+      std::string(JoinProcessor::kQualifiedIdExpr));
+  join_spec.set_child_property_expression("foo");
+  join_spec.mutable_nested_spec()->mutable_scoring_spec()->set_order_by(
+      ScoringSpecProto::Order::ASC);
+
+  // Create and GetChildren should not fail.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<JoinChildrenFetcherImplV3> fetcher,
+      JoinChildrenFetcherImplV3::Create(
+          join_spec, schema_store_.get(), doc_store_.get(),
+          qualified_id_join_index_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          /*child_scored_document_hits=*/
+          {scored_doc_hit_email1, scored_doc_hit_email2,
+           scored_doc_hit_email3}));
+
+  EXPECT_THAT(fetcher->GetChildren(person1_id), IsOkAndHolds(IsEmpty()));
+  EXPECT_THAT(fetcher->GetChildren(person2_id), IsOkAndHolds(IsEmpty()));
+}
+
+TEST_F(JoinChildrenFetcherImplV3Test,
+       GetChildrenShouldSkipExpiredChildDocuments) {
+  fake_clock_.SetSystemTimeMilliseconds(1000);
+
+  // Simulate the following scenario:
+  // - Email child schema has 1 joinable property: "sender" with joinable
+  //   property id 0.
+  //   - email1 joins with person1 by property "sender".
+  //   - email2 joins with person1 by property "sender".
+  //   - email3 joins with person1 by property "sender".
+  // - email1, email2 and email3 are in the child result set.
+  DocumentProto person1 = DocumentBuilder()
+                              .SetKey("pkg$db/namespace", "person1")
+                              .SetSchema("Person")
+                              .SetTtlMs(5000)
+                              .AddStringProperty("Name", "Alice")
+                              .Build();
+
+  DocumentProto email1 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email1")
+          .SetSchema("Email")
+          .SetTtlMs(5000)
+          .AddStringProperty("sender", "pkg$db/namespace#person1")
+          .Build();
+  DocumentProto email2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email2")
+          .SetSchema("Email")
+          .SetTtlMs(5000)
+          .AddStringProperty("sender", "pkg$db/namespace#person1")
+          .Build();
+  DocumentProto email3 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email3")
+          .SetSchema("Email")
+          .SetTtlMs(1000)
+          .AddStringProperty("sender", "pkg$db/namespace#person1")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person1_id,
+                             PutAndIndexDocument(person1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId email1_id, PutAndIndexDocument(email1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId email2_id, PutAndIndexDocument(email2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId email3_id, PutAndIndexDocument(email3));
+
+  ScoredDocumentHit scored_doc_hit_email1(email1_id, kSectionIdMaskNone,
+                                          /*score=*/1.0);
+  ScoredDocumentHit scored_doc_hit_email2(email2_id, kSectionIdMaskNone,
+                                          /*score=*/2.0);
+  ScoredDocumentHit scored_doc_hit_email3(email3_id, kSectionIdMaskNone,
+                                          /*score=*/3.0);
+
+  // Join by property "sender".
+  JoinSpecProto join_spec;
+  join_spec.set_parent_property_expression(
+      std::string(JoinProcessor::kQualifiedIdExpr));
+  join_spec.set_child_property_expression("sender");
+  join_spec.mutable_nested_spec()->mutable_scoring_spec()->set_order_by(
+      ScoringSpecProto::Order::ASC);
+
+  // Adjust the clock to make email3 expired.
+  fake_clock_.SetSystemTimeMilliseconds(3000);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<JoinChildrenFetcherImplV3> fetcher,
+      JoinChildrenFetcherImplV3::Create(
+          join_spec, schema_store_.get(), doc_store_.get(),
+          qualified_id_join_index_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          /*child_scored_document_hits=*/
+          {scored_doc_hit_email1, scored_doc_hit_email2,
+           scored_doc_hit_email3}));
+
+  // Email3 should be skipped since it is expired.
+  //
+  // This scenario is unlikely to happen in practice since expired documents
+  // will mostly not be included in the child result set, but we still want to
+  // make sure an invalid DocumentFilterData is handled correctly.
+  EXPECT_THAT(fetcher->GetChildren(person1_id),
+              IsOkAndHolds(
+                  ElementsAre(EqualsScoredDocumentHit(scored_doc_hit_email1),
+                              EqualsScoredDocumentHit(scored_doc_hit_email2))));
+}
+
+TEST_F(JoinChildrenFetcherImplV3Test, GetChildrenShouldDoLazyLookupAndCache) {
+  // Simulate the following scenario:
+  // - Email child schema has 1 joinable property: "sender" with joinable
+  //   property id 0.
+  //   - email1 joins with person1 by property "sender".
+  //   - email2 joins with person1 by property "sender".
+  //   - email3 joins with person2 by property "sender".
+  // - email1, email2 and email3 are in the child result set.
+  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();
+
+  DocumentProto email1 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email1")
+          .SetSchema("Email")
+          .AddStringProperty("sender", "pkg$db/namespace#person1")
+          .Build();
+  DocumentProto email2 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email2")
+          .SetSchema("Email")
+          .AddStringProperty("sender", "pkg$db/namespace#person1")
+          .Build();
+  DocumentProto email3 =
+      DocumentBuilder()
+          .SetKey("pkg$db/namespace", "email3")
+          .SetSchema("Email")
+          .AddStringProperty("sender", "pkg$db/namespace#person2")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person1_id,
+                             PutAndIndexDocument(person1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId person2_id,
+                             PutAndIndexDocument(person2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId email1_id, PutAndIndexDocument(email1));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId email2_id, PutAndIndexDocument(email2));
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId email3_id, PutAndIndexDocument(email3));
+
+  ScoredDocumentHit scored_doc_hit_email1(email1_id, kSectionIdMaskNone,
+                                          /*score=*/1.0);
+  ScoredDocumentHit scored_doc_hit_email2(email2_id, kSectionIdMaskNone,
+                                          /*score=*/2.0);
+  ScoredDocumentHit scored_doc_hit_email3(email3_id, kSectionIdMaskNone,
+                                          /*score=*/3.0);
+
+  // Join by property "sender".
+  JoinSpecProto join_spec;
+  join_spec.set_parent_property_expression(
+      std::string(JoinProcessor::kQualifiedIdExpr));
+  join_spec.set_child_property_expression("sender");
+  join_spec.mutable_nested_spec()->mutable_scoring_spec()->set_order_by(
+      ScoringSpecProto::Order::ASC);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<JoinChildrenFetcherImplV3> fetcher,
+      JoinChildrenFetcherImplV3::Create(
+          join_spec, schema_store_.get(), doc_store_.get(),
+          qualified_id_join_index_.get(),
+          /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds(),
+          /*child_scored_document_hits=*/
+          {scored_doc_hit_email1, scored_doc_hit_email2,
+           scored_doc_hit_email3}));
+
+  EXPECT_THAT(fetcher->GetChildren(person1_id),
+              IsOkAndHolds(
+                  ElementsAre(EqualsScoredDocumentHit(scored_doc_hit_email1),
+                              EqualsScoredDocumentHit(scored_doc_hit_email2))));
+  EXPECT_THAT(fetcher->GetChildren(person2_id),
+              IsOkAndHolds(
+                  ElementsAre(EqualsScoredDocumentHit(scored_doc_hit_email3))));
+
+  // Intentionally clear the join index.
+  ICING_ASSERT_OK(qualified_id_join_index_->Clear());
+
+  // GetChildren again. The last round lookup should have cached the results, so
+  // even though the join index is cleared, we should still get the same result.
+  EXPECT_THAT(fetcher->GetChildren(person1_id),
+              IsOkAndHolds(
+                  ElementsAre(EqualsScoredDocumentHit(scored_doc_hit_email1),
+                              EqualsScoredDocumentHit(scored_doc_hit_email2))));
+  EXPECT_THAT(fetcher->GetChildren(person2_id),
+              IsOkAndHolds(
+                  ElementsAre(EqualsScoredDocumentHit(scored_doc_hit_email3))));
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/join/join-children-fetcher.cc b/icing/join/join-children-fetcher.cc
deleted file mode 100644
index c6d1b97..0000000
--- a/icing/join/join-children-fetcher.cc
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (C) 2023 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/join-children-fetcher.h"
-
-#include "icing/absl_ports/canonical_errors.h"
-#include "icing/absl_ports/str_cat.h"
-
-namespace icing {
-namespace lib {
-
-libtextclassifier3::StatusOr<std::vector<ScoredDocumentHit>>
-JoinChildrenFetcher::GetChildren(DocumentId parent_doc_id) const {
-  if (join_spec_.parent_property_expression() == kQualifiedIdExpr) {
-    if (auto iter = map_joinable_qualified_id_.find(parent_doc_id);
-        iter != map_joinable_qualified_id_.end()) {
-      return iter->second;
-    }
-    return std::vector<ScoredDocumentHit>();
-  }
-  // TODO(b/256022027): So far we only support kQualifiedIdExpr for
-  // parent_property_expression, we could support more.
-  return absl_ports::UnimplementedError(absl_ports::StrCat(
-      "Parent property expression must be ", kQualifiedIdExpr));
-}
-
-}  // namespace lib
-}  // namespace icing
diff --git a/icing/join/join-children-fetcher.h b/icing/join/join-children-fetcher.h
index 1b875bc..4f9870f 100644
--- a/icing/join/join-children-fetcher.h
+++ b/icing/join/join-children-fetcher.h
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 Google LLC
+// Copyright (C) 2024 Google LLC
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,7 +15,7 @@
 #ifndef ICING_JOIN_JOIN_CHILDREN_FETCHER_H_
 #define ICING_JOIN_JOIN_CHILDREN_FETCHER_H_
 
-#include <unordered_map>
+#include <string_view>
 #include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
@@ -26,22 +26,13 @@
 namespace icing {
 namespace lib {
 
-// A class that provides the GetChildren method for joins to fetch all children
-// documents given a parent document id.
-//
-// Internally, the class maintains a map for each joinable value type that
-// groups children according to the joinable values. Currently we only support
-// QUALIFIED_ID joining, in which the joinable value type is document id.
+// A virtual class that provides the GetChildren method for joins to fetch all
+// children documents given a parent document id.
 class JoinChildrenFetcher {
  public:
-  explicit JoinChildrenFetcher(
-      const JoinSpecProto& join_spec,
-      std::unordered_map<DocumentId, std::vector<ScoredDocumentHit>>&&
-          map_joinable_qualified_id)
-      : join_spec_(join_spec),
-        map_joinable_qualified_id_(std::move(map_joinable_qualified_id)) {}
+  virtual ~JoinChildrenFetcher() = default;
 
-  // Get a vector of children ScoredDocumentHit by parent document id.
+  // Gets a vector of children ScoredDocumentHit by parent document id.
   //
   // TODO(b/256022027): Implement property value joins with types of string and
   // int. In these cases, GetChildren should look up join index to fetch
@@ -50,21 +41,18 @@
   // corresponding map in this class using the joinable property value.
   //
   // Returns:
-  //   The vector of results on success.
-  //   UNIMPLEMENTED_ERROR if the join type specified by join_spec is not
-  //   supported.
-  libtextclassifier3::StatusOr<std::vector<ScoredDocumentHit>> GetChildren(
-      DocumentId parent_doc_id) const;
+  //   - The vector of ScoredDocumentHits for its children on success.
+  //   - Other errors, depending on the implementation.
+  virtual libtextclassifier3::StatusOr<std::vector<ScoredDocumentHit>>
+  GetChildren(DocumentId parent_doc_id) const = 0;
 
- private:
+ protected:
+  explicit JoinChildrenFetcher(const JoinSpecProto& join_spec)
+      : join_spec_(join_spec) {}
+
   static constexpr std::string_view kQualifiedIdExpr = "this.qualifiedId()";
 
   const JoinSpecProto& join_spec_;  // Does not own!
-
-  // The map that groups children by qualified id used to support QualifiedId
-  // joining. The joining type is document id.
-  std::unordered_map<DocumentId, std::vector<ScoredDocumentHit>>
-      map_joinable_qualified_id_;
 };
 
 }  // namespace lib
diff --git a/icing/join/join-processor.cc b/icing/join/join-processor.cc
index 1b7ca0d..38846a4 100644
--- a/icing/join/join-processor.cc
+++ b/icing/join/join-processor.cc
@@ -15,19 +15,25 @@
 #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>
 
+#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/aggregation-scorer.h"
-#include "icing/join/doc-join-info.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"
@@ -38,13 +44,14 @@
 #include "icing/scoring/scored-document-hit.h"
 #include "icing/store/document-filter-data.h"
 #include "icing/store/document-id.h"
-#include "icing/store/namespace-fingerprint-identifier.h"
+#include "icing/store/namespace-id-fingerprint.h"
+#include "icing/util/logging.h"
 #include "icing/util/status-macros.h"
 
 namespace icing {
 namespace lib {
 
-libtextclassifier3::StatusOr<JoinChildrenFetcher>
+libtextclassifier3::StatusOr<std::unique_ptr<JoinChildrenFetcher>>
 JoinProcessor::GetChildrenFetcher(
     const JoinSpecProto& join_spec,
     std::vector<ScoredDocumentHit>&& child_scored_document_hits) {
@@ -55,119 +62,27 @@
         "Parent property expression must be ", kQualifiedIdExpr));
   }
 
+  switch (qualified_id_join_index_->version()) {
+    case QualifiedIdJoinIndex::Version::kV1:
+      return GetChildrenFetcherV1(join_spec,
+                                  std::move(child_scored_document_hits));
+    case QualifiedIdJoinIndex::Version::kV2:
+      return GetChildrenFetcherV2(join_spec,
+                                  std::move(child_scored_document_hits));
+    case QualifiedIdJoinIndex::Version::kV3:
+      return JoinChildrenFetcherImplV3::Create(
+          join_spec, schema_store_, doc_store_, qualified_id_join_index_,
+          current_time_ms_, std::move(child_scored_document_hits));
+  }
+}
+
+libtextclassifier3::StatusOr<std::unique_ptr<JoinChildrenFetcher>>
+JoinProcessor::GetChildrenFetcherV1(
+    const JoinSpecProto& join_spec,
+    std::vector<ScoredDocumentHit>&& child_scored_document_hits) {
   ScoredDocumentHitComparator score_comparator(
       /*is_descending=*/join_spec.nested_spec().scoring_spec().order_by() ==
       ScoringSpecProto::Order::DESC);
-
-  if (qualified_id_join_index_->is_v2()) {
-    // v2
-    // Step 1a: sort child ScoredDocumentHits in document id descending order.
-    std::sort(child_scored_document_hits.begin(),
-              child_scored_document_hits.end(),
-              [](const ScoredDocumentHit& lhs, const ScoredDocumentHit& rhs) {
-                return lhs.document_id() > rhs.document_id();
-              });
-
-    // Step 1b: group all child ScoredDocumentHits by the document's
-    //          schema_type_id.
-    std::unordered_map<SchemaTypeId, std::vector<ScoredDocumentHit>>
-        schema_to_child_scored_doc_hits_map;
-    for (const ScoredDocumentHit& child_scored_document_hit :
-         child_scored_document_hits) {
-      std::optional<DocumentFilterData> child_doc_filter_data =
-          doc_store_->GetAliveDocumentFilterData(
-              child_scored_document_hit.document_id(), current_time_ms_);
-      if (!child_doc_filter_data) {
-        continue;
-      }
-
-      schema_to_child_scored_doc_hits_map[child_doc_filter_data
-                                              ->schema_type_id()]
-          .push_back(child_scored_document_hit);
-    }
-
-    // Step 1c: for each schema_type_id, lookup QualifiedIdJoinIndexImplV2 to
-    //          fetch all child join data from posting list(s). Convert all
-    //          child join data to referenced parent document ids and bucketize
-    //          child ScoredDocumentHits by it.
-    std::unordered_map<DocumentId, std::vector<ScoredDocumentHit>>
-        parent_to_child_docs_map;
-    for (auto& [schema_type_id, grouped_child_scored_doc_hits] :
-         schema_to_child_scored_doc_hits_map) {
-      // Get joinable_property_id of this schema.
-      ICING_ASSIGN_OR_RETURN(
-          const JoinablePropertyMetadata* metadata,
-          schema_store_->GetJoinablePropertyMetadata(
-              schema_type_id, join_spec.child_property_expression()));
-      if (metadata == nullptr ||
-          metadata->value_type != JoinableConfig::ValueType::QUALIFIED_ID) {
-        // Currently we only support qualified id, so skip other types.
-        continue;
-      }
-
-      // Lookup QualifiedIdJoinIndexImplV2.
-      ICING_ASSIGN_OR_RETURN(
-          std::unique_ptr<QualifiedIdJoinIndex::JoinDataIteratorBase>
-              join_index_iter,
-          qualified_id_join_index_->GetIterator(
-              schema_type_id, /*joinable_property_id=*/metadata->id));
-
-      // - Join index contains all join data of schema_type_id and
-      //   join_index_iter will return all of them in (child) document id
-      //   descending order.
-      // - But we only need join data of child document ids which appear in
-      //   grouped_child_scored_doc_hits. Also grouped_child_scored_doc_hits
-      //   contain ScoredDocumentHits in (child) document id descending order.
-      // - Therefore, we advance 2 iterators to intersect them and get desired
-      //   join data.
-      auto child_scored_doc_hits_iter = grouped_child_scored_doc_hits.cbegin();
-      while (join_index_iter->Advance().ok() &&
-             child_scored_doc_hits_iter !=
-                 grouped_child_scored_doc_hits.cend()) {
-        // Advance child_scored_doc_hits_iter until it points to a
-        // ScoredDocumentHit with document id <= the one pointed by
-        // join_index_iter.
-        while (child_scored_doc_hits_iter !=
-                   grouped_child_scored_doc_hits.cend() &&
-               child_scored_doc_hits_iter->document_id() >
-                   join_index_iter->GetCurrent().document_id()) {
-          ++child_scored_doc_hits_iter;
-        }
-
-        if (child_scored_doc_hits_iter !=
-                grouped_child_scored_doc_hits.cend() &&
-            child_scored_doc_hits_iter->document_id() ==
-                join_index_iter->GetCurrent().document_id()) {
-          // We get a join data whose child document id exists in both join
-          // index and grouped_child_scored_doc_hits. Convert its join info to
-          // referenced parent document ids and bucketize ScoredDocumentHits by
-          // it (putting into parent_to_child_docs_map).
-          const NamespaceFingerprintIdentifier& ref_ns_id =
-              join_index_iter->GetCurrent().join_info();
-          libtextclassifier3::StatusOr<DocumentId> ref_parent_doc_id_or =
-              doc_store_->GetDocumentId(ref_ns_id);
-          if (ref_parent_doc_id_or.ok()) {
-            parent_to_child_docs_map[std::move(ref_parent_doc_id_or)
-                                         .ValueOrDie()]
-                .push_back(*child_scored_doc_hits_iter);
-          }
-        }
-      }
-    }
-
-    // Step 1d: finally, sort each parent's joined child ScoredDocumentHits by
-    //          score.
-    for (auto& [parent_doc_id, bucketized_child_scored_hits] :
-         parent_to_child_docs_map) {
-      std::sort(bucketized_child_scored_hits.begin(),
-                bucketized_child_scored_hits.end(), score_comparator);
-    }
-
-    return JoinChildrenFetcher(join_spec, std::move(parent_to_child_docs_map));
-  }
-
-  // v1
-  // TODO(b/275121148): deprecate this part after rollout v2.
   std::sort(child_scored_document_hits.begin(),
             child_scored_document_hits.end(), score_comparator);
 
@@ -191,7 +106,117 @@
 
     map_joinable_qualified_id[ref_doc_id].push_back(child);
   }
-  return JoinChildrenFetcher(join_spec, std::move(map_joinable_qualified_id));
+  return JoinChildrenFetcherImplDeprecated::Create(
+      join_spec, std::move(map_joinable_qualified_id));
+}
+
+libtextclassifier3::StatusOr<std::unique_ptr<JoinChildrenFetcher>>
+JoinProcessor::GetChildrenFetcherV2(
+    const JoinSpecProto& join_spec,
+    std::vector<ScoredDocumentHit>&& child_scored_document_hits) {
+  // Step 1a: sort child ScoredDocumentHits in document id descending order.
+  std::sort(child_scored_document_hits.begin(),
+            child_scored_document_hits.end(),
+            [](const ScoredDocumentHit& lhs, const ScoredDocumentHit& rhs) {
+              return lhs.document_id() > rhs.document_id();
+            });
+
+  // Step 1b: group all child ScoredDocumentHits by the document's
+  //          schema_type_id.
+  std::unordered_map<SchemaTypeId, std::vector<ScoredDocumentHit>>
+      schema_to_child_scored_doc_hits_map;
+  for (const ScoredDocumentHit& child_scored_document_hit :
+       child_scored_document_hits) {
+    std::optional<DocumentFilterData> child_doc_filter_data =
+        doc_store_->GetAliveDocumentFilterData(
+            child_scored_document_hit.document_id(), current_time_ms_);
+    if (!child_doc_filter_data) {
+      continue;
+    }
+
+    schema_to_child_scored_doc_hits_map[child_doc_filter_data->schema_type_id()]
+        .push_back(child_scored_document_hit);
+  }
+
+  // Step 1c: for each schema_type_id, lookup QualifiedIdJoinIndexImplV2 to
+  //          fetch all child join data from posting list(s). Convert all
+  //          child join data to referenced parent document ids and bucketize
+  //          child ScoredDocumentHits by it.
+  std::unordered_map<DocumentId, std::vector<ScoredDocumentHit>>
+      parent_to_child_docs_map;
+  for (auto& [schema_type_id, grouped_child_scored_doc_hits] :
+       schema_to_child_scored_doc_hits_map) {
+    // Get joinable_property_id of this schema.
+    ICING_ASSIGN_OR_RETURN(
+        const JoinablePropertyMetadata* metadata,
+        schema_store_->GetJoinablePropertyMetadata(
+            schema_type_id, join_spec.child_property_expression()));
+    if (metadata == nullptr ||
+        metadata->value_type != JoinableConfig::ValueType::QUALIFIED_ID) {
+      // Currently we only support qualified id, so skip other types.
+      continue;
+    }
+
+    // Lookup QualifiedIdJoinIndexImplV2.
+    ICING_ASSIGN_OR_RETURN(
+        std::unique_ptr<QualifiedIdJoinIndex::JoinDataIteratorBase>
+            join_index_iter,
+        qualified_id_join_index_->GetIterator(
+            schema_type_id, /*joinable_property_id=*/metadata->id));
+
+    // - Join index contains all join data of schema_type_id and
+    //   join_index_iter will return all of them in (child) document id
+    //   descending order.
+    // - But we only need join data of child document ids which appear in
+    //   grouped_child_scored_doc_hits. Also grouped_child_scored_doc_hits
+    //   contain ScoredDocumentHits in (child) document id descending order.
+    // - Therefore, we advance 2 iterators to intersect them and get desired
+    //   join data.
+    auto child_scored_doc_hits_iter = grouped_child_scored_doc_hits.cbegin();
+    while (join_index_iter->Advance().ok() &&
+           child_scored_doc_hits_iter != grouped_child_scored_doc_hits.cend()) {
+      // Advance child_scored_doc_hits_iter until it points to a
+      // ScoredDocumentHit with document id <= the one pointed by
+      // join_index_iter.
+      while (child_scored_doc_hits_iter !=
+                 grouped_child_scored_doc_hits.cend() &&
+             child_scored_doc_hits_iter->document_id() >
+                 join_index_iter->GetCurrent().document_id()) {
+        ++child_scored_doc_hits_iter;
+      }
+
+      if (child_scored_doc_hits_iter != grouped_child_scored_doc_hits.cend() &&
+          child_scored_doc_hits_iter->document_id() ==
+              join_index_iter->GetCurrent().document_id()) {
+        // We get a join data whose child document id exists in both join
+        // index and grouped_child_scored_doc_hits. Convert its join info to
+        // referenced parent document ids and bucketize ScoredDocumentHits by
+        // it (putting into parent_to_child_docs_map).
+        const NamespaceIdFingerprint& ref_doc_nsid_uri_fingerprint =
+            join_index_iter->GetCurrent().join_info();
+        libtextclassifier3::StatusOr<DocumentId> ref_parent_doc_id_or =
+            doc_store_->GetDocumentId(ref_doc_nsid_uri_fingerprint);
+        if (ref_parent_doc_id_or.ok()) {
+          parent_to_child_docs_map[std::move(ref_parent_doc_id_or).ValueOrDie()]
+              .push_back(*child_scored_doc_hits_iter);
+        }
+      }
+    }
+  }
+
+  // Step 1d: finally, sort each parent's joined child ScoredDocumentHits by
+  //          score.
+  ScoredDocumentHitComparator score_comparator(
+      /*is_descending=*/join_spec.nested_spec().scoring_spec().order_by() ==
+      ScoringSpecProto::Order::DESC);
+  for (auto& [parent_doc_id, bucketized_child_scored_hits] :
+       parent_to_child_docs_map) {
+    std::sort(bucketized_child_scored_hits.begin(),
+              bucketized_child_scored_hits.end(), score_comparator);
+  }
+
+  return JoinChildrenFetcherImplDeprecated::Create(
+      join_spec, std::move(parent_to_child_docs_map));
 }
 
 libtextclassifier3::StatusOr<std::vector<JoinedScoredDocumentHit>>
@@ -221,25 +246,103 @@
   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;
+}
+
 libtextclassifier3::StatusOr<DocumentId>
 JoinProcessor::FetchReferencedQualifiedId(
-    const DocumentId& document_id, const std::string& property_path) const {
-  std::optional<DocumentFilterData> filter_data =
-      doc_store_->GetAliveDocumentFilterData(document_id, current_time_ms_);
-  if (!filter_data) {
+    const DocumentId& child_document_id,
+    const std::string& property_path) const {
+  std::optional<DocumentFilterData> child_filter_data =
+      doc_store_->GetAliveDocumentFilterData(child_document_id,
+                                             current_time_ms_);
+  if (!child_filter_data) {
     return kInvalidDocumentId;
   }
 
-  ICING_ASSIGN_OR_RETURN(const JoinablePropertyMetadata* metadata,
-                         schema_store_->GetJoinablePropertyMetadata(
-                             filter_data->schema_type_id(), property_path));
+  ICING_ASSIGN_OR_RETURN(
+      const JoinablePropertyMetadata* metadata,
+      schema_store_->GetJoinablePropertyMetadata(
+          child_filter_data->schema_type_id(), property_path));
   if (metadata == nullptr ||
       metadata->value_type != JoinableConfig::ValueType::QUALIFIED_ID) {
     // Currently we only support qualified id.
     return kInvalidDocumentId;
   }
 
-  DocJoinInfo info(document_id, metadata->id);
+  DocumentJoinIdPair info(child_document_id, metadata->id);
   libtextclassifier3::StatusOr<std::string_view> ref_qualified_id_str_or =
       qualified_id_join_index_->Get(info);
   if (!ref_qualified_id_str_or.ok()) {
diff --git a/icing/join/join-processor.h b/icing/join/join-processor.h
index 517e9db..8174693 100644
--- a/icing/join/join-processor.h
+++ b/icing/join/join-processor.h
@@ -16,16 +16,20 @@
 #define ICING_JOIN_JOIN_PROCESSOR_H_
 
 #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 {
@@ -44,14 +48,15 @@
         qualified_id_join_index_(qualified_id_join_index),
         current_time_ms_(current_time_ms) {}
 
-  // Get a JoinChildrenFetcher used to fetch all children documents by a parent
+  // Gets a JoinChildrenFetcher used to fetch all children documents by a parent
   // document id.
   //
   // Returns:
-  //   A JoinChildrenFetcher instance on success.
+  //   std::unique_ptr<JoinChildrenFetcher> instance on success.
   //   UNIMPLEMENTED_ERROR if the join type specified by join_spec is not
   //   supported.
-  libtextclassifier3::StatusOr<JoinChildrenFetcher> GetChildrenFetcher(
+  libtextclassifier3::StatusOr<std::unique_ptr<JoinChildrenFetcher>>
+  GetChildrenFetcher(
       const JoinSpecProto& join_spec,
       std::vector<ScoredDocumentHit>&& child_scored_document_hits);
 
@@ -60,7 +65,37 @@
       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 v1, v2 after rollout v3.
+
+  // Helper function to construct JoinChildrenFetcher for
+  // QualfiedIdJoinIndexImplV1.
+  //
+  // Note: JoinChildrenFetcherImplDeprecated will be returned.
+  libtextclassifier3::StatusOr<std::unique_ptr<JoinChildrenFetcher>>
+  GetChildrenFetcherV1(
+      const JoinSpecProto& join_spec,
+      std::vector<ScoredDocumentHit>&& child_scored_document_hits);
+
+  // Helper function to construct JoinChildrenFetcher for
+  // QualfiedIdJoinIndexImplV2.
+  //
+  // Note: JoinChildrenFetcherImplDeprecated will be returned.
+  libtextclassifier3::StatusOr<std::unique_ptr<JoinChildrenFetcher>>
+  GetChildrenFetcherV2(
+      const JoinSpecProto& join_spec,
+      std::vector<ScoredDocumentHit>&& child_scored_document_hits);
+
   // Fetches referenced document id of the given document under the given
   // property path.
   //
diff --git a/icing/join/join-processor_test.cc b/icing/join/join-processor_test.cc
index a205125..2cc1e25 100644
--- a/icing/join/join-processor_test.cc
+++ b/icing/join/join-processor_test.cc
@@ -14,21 +14,28 @@
 
 #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"
 #include "icing/absl_ports/canonical_errors.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/join-children-fetcher.h"
 #include "icing/join/qualified-id-join-index-impl-v1.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/platform.h"
@@ -45,11 +52,12 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.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"
@@ -60,14 +68,22 @@
 namespace {
 
 using ::testing::ElementsAre;
+using ::testing::Eq;
+using ::testing::HasSubstr;
+using ::testing::IsEmpty;
 using ::testing::IsTrue;
+using ::testing::Ne;
+using ::testing::UnorderedElementsAre;
 
 // TODO(b/275121148): remove template after deprecating
 // QualifiedIdJoinIndexImplV1.
 template <typename T>
 class JoinProcessorTest : public ::testing::Test {
  protected:
+  using JOIN_INDEX_TYPE = T;
+
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     test_dir_ = GetTestTempDir() + "/icing_join_processor_test";
     ASSERT_THAT(filesystem_.CreateDirectoryRecursively(test_dir_.c_str()),
                 IsTrue());
@@ -79,7 +95,7 @@
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
 
@@ -92,8 +108,8 @@
         filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str()),
         IsTrue());
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
 
     SchemaProto schema =
         SchemaBuilder()
@@ -124,14 +140,42 @@
                                                         TOKENIZER_PLAIN)
                                      .SetCardinality(CARDINALITY_OPTIONAL))
                     .AddProperty(PropertyConfigBuilder()
-                                     .SetName("sender")
-                                     .SetDataTypeJoinableString(
-                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID)
-                                     .SetCardinality(CARDINALITY_OPTIONAL))
-                    .AddProperty(PropertyConfigBuilder()
                                      .SetName("receiver")
                                      .SetDataTypeJoinableString(
-                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                         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();
@@ -144,14 +188,14 @@
                 IsTrue());
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(
-            &filesystem_, doc_store_dir_, &fake_clock_, schema_store_.get(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     doc_store_ = std::move(create_result.document_store);
 
     ICING_ASSERT_OK_AND_ASSIGN(qualified_id_join_index_,
@@ -188,11 +232,17 @@
                                               /*pre_mapping_fbv=*/false);
   }
 
+  template <>
+  libtextclassifier3::StatusOr<std::unique_ptr<QualifiedIdJoinIndex>>
+  CreateQualifiedIdJoinIndex<QualifiedIdJoinIndexImplV3>() {
+    return QualifiedIdJoinIndexImplV3::Create(
+        filesystem_, qualified_id_join_index_dir_, *feature_flags_);
+  }
+
   libtextclassifier3::StatusOr<DocumentId> PutAndIndexDocument(
       const DocumentProto& document) {
     ICING_ASSIGN_OR_RETURN(DocumentStore::PutResult put_result,
                            doc_store_->Put(document));
-    DocumentId document_id = put_result.new_document_id;
     ICING_ASSIGN_OR_RETURN(
         TokenizedDocument tokenized_document,
         TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
@@ -202,10 +252,11 @@
         std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
         QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
                                                qualified_id_join_index_.get()));
-    ICING_RETURN_IF_ERROR(handler->Handle(tokenized_document, document_id,
-                                          /*recovery_mode=*/false,
-                                          /*put_document_stats=*/nullptr));
-    return document_id;
+    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;
   }
 
   libtextclassifier3::StatusOr<std::vector<JoinedScoredDocumentHit>> Join(
@@ -216,14 +267,15 @@
         doc_store_.get(), schema_store_.get(), qualified_id_join_index_.get(),
         /*current_time_ms=*/fake_clock_.GetSystemTimeMilliseconds());
     ICING_ASSIGN_OR_RETURN(
-        JoinChildrenFetcher join_children_fetcher,
+        std::unique_ptr<JoinChildrenFetcher> join_children_fetcher,
         join_processor.GetChildrenFetcher(
             join_spec, std::move(child_scored_document_hits)));
     return join_processor.Join(join_spec,
                                std::move(parent_scored_document_hits),
-                               join_children_fetcher);
+                               *join_children_fetcher);
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   std::string test_dir_;
   std::string schema_store_dir_;
@@ -239,7 +291,8 @@
 };
 
 using TestTypes =
-    ::testing::Types<QualifiedIdJoinIndexImplV1, QualifiedIdJoinIndexImplV2>;
+    ::testing::Types<QualifiedIdJoinIndexImplV1, QualifiedIdJoinIndexImplV2,
+                     QualifiedIdJoinIndexImplV3>;
 TYPED_TEST_SUITE(JoinProcessorTest, TestTypes);
 
 TYPED_TEST(JoinProcessorTest, JoinByQualifiedId_allDocuments) {
@@ -921,6 +974,679 @@
                       /*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/posting-list-join-data-accessor_test.cc b/icing/join/posting-list-join-data-accessor_test.cc
index ddc2d32..1775074 100644
--- a/icing/join/posting-list-join-data-accessor_test.cc
+++ b/icing/join/posting-list-join-data-accessor_test.cc
@@ -31,7 +31,7 @@
 #include "icing/join/document-id-to-join-info.h"
 #include "icing/join/posting-list-join-data-serializer.h"
 #include "icing/store/document-id.h"
-#include "icing/store/namespace-fingerprint-identifier.h"
+#include "icing/store/namespace-id-fingerprint.h"
 #include "icing/store/namespace-id.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/tmp-directory.h"
@@ -48,7 +48,7 @@
 using ::testing::Ne;
 using ::testing::SizeIs;
 
-using JoinDataType = DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>;
+using JoinDataType = DocumentIdToJoinInfo<NamespaceIdFingerprint>;
 
 static constexpr NamespaceId kDefaultNamespaceId = 1;
 
@@ -92,8 +92,8 @@
   for (int i = 0; i < num_data; ++i) {
     data.push_back(JoinDataType(
         start_document_id,
-        NamespaceFingerprintIdentifier(ref_namespace_id,
-                                       /*fingerprint=*/start_ref_hash_uri)));
+        NamespaceIdFingerprint(ref_namespace_id,
+                               /*fingerprint=*/start_ref_hash_uri)));
 
     ++start_document_id;
     ++start_ref_hash_uri;
@@ -137,7 +137,7 @@
   // Add a single data. This will fit in a min-sized posting list.
   JoinDataType data1(
       /*document_id=*/1,
-      NamespaceFingerprintIdentifier(kDefaultNamespaceId, /*fingerprint=*/123));
+      NamespaceIdFingerprint(kDefaultNamespaceId, /*fingerprint=*/123));
   ICING_ASSERT_OK(pl_accessor->PrependData(data1));
   PostingListAccessor::FinalizeResult result1 =
       std::move(*pl_accessor).Finalize();
@@ -154,7 +154,7 @@
           flash_index_storage_.get(), serializer_.get(), result1.id));
   JoinDataType data2(
       /*document_id=*/2,
-      NamespaceFingerprintIdentifier(kDefaultNamespaceId, /*fingerprint=*/456));
+      NamespaceIdFingerprint(kDefaultNamespaceId, /*fingerprint=*/456));
   ICING_ASSERT_OK(pl_accessor->PrependData(data2));
   PostingListAccessor::FinalizeResult result2 =
       std::move(*pl_accessor).Finalize();
@@ -372,24 +372,24 @@
           flash_index_storage_.get(), serializer_.get()));
   JoinDataType data1(
       /*document_id=*/1,
-      NamespaceFingerprintIdentifier(kDefaultNamespaceId, /*fingerprint=*/819));
+      NamespaceIdFingerprint(kDefaultNamespaceId, /*fingerprint=*/819));
   ICING_ASSERT_OK(pl_accessor->PrependData(data1));
 
   JoinDataType data2(
       /*document_id=*/1,
-      NamespaceFingerprintIdentifier(kDefaultNamespaceId, /*fingerprint=*/818));
+      NamespaceIdFingerprint(kDefaultNamespaceId, /*fingerprint=*/818));
   EXPECT_THAT(pl_accessor->PrependData(data2),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
-  JoinDataType data3(/*document_id=*/1,
-                     NamespaceFingerprintIdentifier(kDefaultNamespaceId - 1,
-                                                    /*fingerprint=*/820));
+  JoinDataType data3(
+      /*document_id=*/1,
+      NamespaceIdFingerprint(kDefaultNamespaceId - 1, /*fingerprint=*/820));
   EXPECT_THAT(pl_accessor->PrependData(data3),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
-  JoinDataType data4(/*document_id=*/0,
-                     NamespaceFingerprintIdentifier(kDefaultNamespaceId + 1,
-                                                    /*fingerprint=*/820));
+  JoinDataType data4(
+      /*document_id=*/0,
+      NamespaceIdFingerprint(kDefaultNamespaceId + 1, /*fingerprint=*/820));
   EXPECT_THAT(pl_accessor->PrependData(data4),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
@@ -414,7 +414,7 @@
           flash_index_storage_.get(), serializer_.get()));
   JoinDataType data1(
       /*document_id=*/1,
-      NamespaceFingerprintIdentifier(kDefaultNamespaceId, /*fingerprint=*/819));
+      NamespaceIdFingerprint(kDefaultNamespaceId, /*fingerprint=*/819));
   ICING_ASSERT_OK(pl_accessor1->PrependData(data1));
   PostingListAccessor::FinalizeResult result1 =
       std::move(*pl_accessor1).Finalize();
diff --git a/icing/join/posting-list-join-data-serializer.h b/icing/join/posting-list-join-data-serializer.h
index 9f39dca..6778f61 100644
--- a/icing/join/posting-list-join-data-serializer.h
+++ b/icing/join/posting-list-join-data-serializer.h
@@ -33,7 +33,7 @@
 namespace lib {
 
 // A serializer class to serialize JoinDataType to PostingListUsed. Usually
-// JoinDataType is DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>,
+// JoinDataType is DocumentIdToJoinInfo<NamespaceIdFingerprint>,
 // DocumentIdToJoinInfo<TermId>, or DocumentIdToJoinInfo<int64_t>.
 //
 // REQUIRES:
diff --git a/icing/join/posting-list-join-data-serializer_test.cc b/icing/join/posting-list-join-data-serializer_test.cc
index 20137b6..893cc68 100644
--- a/icing/join/posting-list-join-data-serializer_test.cc
+++ b/icing/join/posting-list-join-data-serializer_test.cc
@@ -23,7 +23,7 @@
 #include "gtest/gtest.h"
 #include "icing/file/posting_list/posting-list-used.h"
 #include "icing/join/document-id-to-join-info.h"
-#include "icing/store/namespace-fingerprint-identifier.h"
+#include "icing/store/namespace-id-fingerprint.h"
 #include "icing/testing/common-matchers.h"
 
 using testing::ElementsAre;
@@ -38,152 +38,135 @@
 namespace {
 
 TEST(PostingListJoinDataSerializerTest, GetMinPostingListSizeToFitNotNull) {
-  PostingListJoinDataSerializer<
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>>
+  PostingListJoinDataSerializer<DocumentIdToJoinInfo<NamespaceIdFingerprint>>
       serializer;
 
-  int size =
-      2551 * sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>);
+  int size = 2551 * sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>);
   ICING_ASSERT_OK_AND_ASSIGN(
       PostingListUsed pl_used,
       PostingListUsed::CreateFromUnitializedRegion(&serializer, size));
 
-  ASSERT_THAT(
-      serializer.PrependData(
-          &pl_used,
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/0, NamespaceFingerprintIdentifier(
-                                     /*namespace_id=*/1, /*fingerprint=*/2))),
-      IsOk());
-  EXPECT_THAT(
-      serializer.GetMinPostingListSizeToFit(&pl_used),
-      Eq(2 * sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>)));
+  ASSERT_THAT(serializer.PrependData(
+                  &pl_used, DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                /*document_id=*/0,
+                                NamespaceIdFingerprint(/*namespace_id=*/1,
+                                                       /*fingerprint=*/2))),
+              IsOk());
+  EXPECT_THAT(serializer.GetMinPostingListSizeToFit(&pl_used),
+              Eq(2 * sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>)));
 
-  ASSERT_THAT(
-      serializer.PrependData(
-          &pl_used,
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/1, NamespaceFingerprintIdentifier(
-                                     /*namespace_id=*/1, /*fingerprint=*/5))),
-      IsOk());
-  EXPECT_THAT(
-      serializer.GetMinPostingListSizeToFit(&pl_used),
-      Eq(3 * sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>)));
+  ASSERT_THAT(serializer.PrependData(
+                  &pl_used, DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                /*document_id=*/1,
+                                NamespaceIdFingerprint(/*namespace_id=*/1,
+                                                       /*fingerprint=*/5))),
+              IsOk());
+  EXPECT_THAT(serializer.GetMinPostingListSizeToFit(&pl_used),
+              Eq(3 * sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>)));
 }
 
 TEST(PostingListJoinDataSerializerTest, GetMinPostingListSizeToFitAlmostFull) {
-  PostingListJoinDataSerializer<
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>>
+  PostingListJoinDataSerializer<DocumentIdToJoinInfo<NamespaceIdFingerprint>>
       serializer;
 
-  int size = 3 * sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>);
+  int size = 3 * sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>);
   ICING_ASSERT_OK_AND_ASSIGN(
       PostingListUsed pl_used,
       PostingListUsed::CreateFromUnitializedRegion(&serializer, size));
 
-  ASSERT_THAT(
-      serializer.PrependData(
-          &pl_used,
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/0, NamespaceFingerprintIdentifier(
-                                     /*namespace_id=*/1, /*fingerprint=*/2))),
-      IsOk());
-  ASSERT_THAT(
-      serializer.PrependData(
-          &pl_used,
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/1, NamespaceFingerprintIdentifier(
-                                     /*namespace_id=*/1, /*fingerprint=*/5))),
-      IsOk());
+  ASSERT_THAT(serializer.PrependData(
+                  &pl_used, DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                /*document_id=*/0,
+                                NamespaceIdFingerprint(/*namespace_id=*/1,
+                                                       /*fingerprint=*/2))),
+              IsOk());
+  ASSERT_THAT(serializer.PrependData(
+                  &pl_used, DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                /*document_id=*/1,
+                                NamespaceIdFingerprint(/*namespace_id=*/1,
+                                                       /*fingerprint=*/5))),
+              IsOk());
   EXPECT_THAT(serializer.GetMinPostingListSizeToFit(&pl_used), Eq(size));
 }
 
 TEST(PostingListJoinDataSerializerTest, GetMinPostingListSizeToFitFull) {
-  PostingListJoinDataSerializer<
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>>
+  PostingListJoinDataSerializer<DocumentIdToJoinInfo<NamespaceIdFingerprint>>
       serializer;
 
-  int size = 3 * sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>);
+  int size = 3 * sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>);
   ICING_ASSERT_OK_AND_ASSIGN(
       PostingListUsed pl_used,
       PostingListUsed::CreateFromUnitializedRegion(&serializer, size));
 
-  ASSERT_THAT(
-      serializer.PrependData(
-          &pl_used,
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/0, NamespaceFingerprintIdentifier(
-                                     /*namespace_id=*/1, /*fingerprint=*/2))),
-      IsOk());
-  ASSERT_THAT(
-      serializer.PrependData(
-          &pl_used,
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/1, NamespaceFingerprintIdentifier(
-                                     /*namespace_id=*/1, /*fingerprint=*/5))),
-      IsOk());
-  ASSERT_THAT(
-      serializer.PrependData(
-          &pl_used,
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/2, NamespaceFingerprintIdentifier(
-                                     /*namespace_id=*/1, /*fingerprint=*/10))),
-      IsOk());
+  ASSERT_THAT(serializer.PrependData(
+                  &pl_used, DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                /*document_id=*/0,
+                                NamespaceIdFingerprint(/*namespace_id=*/1,
+                                                       /*fingerprint=*/2))),
+              IsOk());
+  ASSERT_THAT(serializer.PrependData(
+                  &pl_used, DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                /*document_id=*/1,
+                                NamespaceIdFingerprint(/*namespace_id=*/1,
+                                                       /*fingerprint=*/5))),
+              IsOk());
+  ASSERT_THAT(serializer.PrependData(
+                  &pl_used, DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                /*document_id=*/2,
+                                NamespaceIdFingerprint(/*namespace_id=*/1,
+                                                       /*fingerprint=*/10))),
+              IsOk());
   EXPECT_THAT(serializer.GetMinPostingListSizeToFit(&pl_used), Eq(size));
 }
 
 TEST(PostingListJoinDataSerializerTest, PrependDataNotFull) {
-  PostingListJoinDataSerializer<
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>>
+  PostingListJoinDataSerializer<DocumentIdToJoinInfo<NamespaceIdFingerprint>>
       serializer;
 
-  int size =
-      2551 * sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>);
+  int size = 2551 * sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>);
   ICING_ASSERT_OK_AND_ASSIGN(
       PostingListUsed pl_used,
       PostingListUsed::CreateFromUnitializedRegion(&serializer, size));
 
   // Make used.
-  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier> data0(
+  DocumentIdToJoinInfo<NamespaceIdFingerprint> data0(
       /*document_id=*/0,
-      NamespaceFingerprintIdentifier(/*namespace_id=*/1, /*fingerprint=*/2));
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/2));
   EXPECT_THAT(serializer.PrependData(&pl_used, data0), IsOk());
   // Size = sizeof(uncompressed data0)
-  int expected_size =
-      sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>);
+  int expected_size = sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>);
   EXPECT_THAT(serializer.GetBytesUsed(&pl_used), Eq(expected_size));
   EXPECT_THAT(serializer.GetData(&pl_used), IsOkAndHolds(ElementsAre(data0)));
 
-  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier> data1(
+  DocumentIdToJoinInfo<NamespaceIdFingerprint> data1(
       /*document_id=*/1,
-      NamespaceFingerprintIdentifier(/*namespace_id=*/1, /*fingerprint=*/5));
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/5));
   EXPECT_THAT(serializer.PrependData(&pl_used, data1), IsOk());
   // Size = sizeof(uncompressed data1)
   //        + sizeof(uncompressed data0)
-  expected_size += sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>);
+  expected_size += sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>);
   EXPECT_THAT(serializer.GetBytesUsed(&pl_used), Eq(expected_size));
   EXPECT_THAT(serializer.GetData(&pl_used),
               IsOkAndHolds(ElementsAre(data1, data0)));
 
-  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier> data2(
-      /*document_id=*/2, NamespaceFingerprintIdentifier(
-                             /*namespace_id=*/1, /*fingerprint=*/10));
+  DocumentIdToJoinInfo<NamespaceIdFingerprint> data2(
+      /*document_id=*/2,
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/10));
   EXPECT_THAT(serializer.PrependData(&pl_used, data2), IsOk());
   // Size = sizeof(uncompressed data2)
   //        + sizeof(uncompressed data1)
   //        + sizeof(uncompressed data0)
-  expected_size += sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>);
+  expected_size += sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>);
   EXPECT_THAT(serializer.GetBytesUsed(&pl_used), Eq(expected_size));
   EXPECT_THAT(serializer.GetData(&pl_used),
               IsOkAndHolds(ElementsAre(data2, data1, data0)));
 }
 
 TEST(PostingListJoinDataSerializerTest, PrependDataAlmostFull) {
-  PostingListJoinDataSerializer<
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>>
+  PostingListJoinDataSerializer<DocumentIdToJoinInfo<NamespaceIdFingerprint>>
       serializer;
 
-  int size = 4 * sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>);
+  int size = 4 * sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>);
   ICING_ASSERT_OK_AND_ASSIGN(
       PostingListUsed pl_used,
       PostingListUsed::CreateFromUnitializedRegion(&serializer, size));
@@ -192,64 +175,61 @@
   // Transitions:
   // Adding data0: EMPTY -> NOT_FULL
   // Adding data1: NOT_FULL -> NOT_FULL
-  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier> data0(
+  DocumentIdToJoinInfo<NamespaceIdFingerprint> data0(
       /*document_id=*/0,
-      NamespaceFingerprintIdentifier(/*namespace_id=*/1, /*fingerprint=*/2));
-  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier> data1(
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/2));
+  DocumentIdToJoinInfo<NamespaceIdFingerprint> data1(
       /*document_id=*/1,
-      NamespaceFingerprintIdentifier(/*namespace_id=*/1, /*fingerprint=*/5));
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/5));
   EXPECT_THAT(serializer.PrependData(&pl_used, data0), IsOk());
   EXPECT_THAT(serializer.PrependData(&pl_used, data1), IsOk());
-  int expected_size =
-      2 * sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>);
+  int expected_size = 2 * sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>);
   EXPECT_THAT(serializer.GetBytesUsed(&pl_used), Eq(expected_size));
   EXPECT_THAT(serializer.GetData(&pl_used),
               IsOkAndHolds(ElementsAre(data1, data0)));
 
   // Add one more data to transition NOT_FULL -> ALMOST_FULL
-  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier> data2(
-      /*document_id=*/2, NamespaceFingerprintIdentifier(
-                             /*namespace_id=*/1, /*fingerprint=*/10));
+  DocumentIdToJoinInfo<NamespaceIdFingerprint> data2(
+      /*document_id=*/2,
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/10));
   EXPECT_THAT(serializer.PrependData(&pl_used, data2), IsOk());
-  expected_size =
-      3 * sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>);
+  expected_size = 3 * sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>);
   EXPECT_THAT(serializer.GetBytesUsed(&pl_used), Eq(expected_size));
   EXPECT_THAT(serializer.GetData(&pl_used),
               IsOkAndHolds(ElementsAre(data2, data1, data0)));
 
   // Add one more data to transition ALMOST_FULL -> FULL
-  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier> data3(
-      /*document_id=*/3, NamespaceFingerprintIdentifier(
-                             /*namespace_id=*/1, /*fingerprint=*/0));
+  DocumentIdToJoinInfo<NamespaceIdFingerprint> data3(
+      /*document_id=*/3,
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/0));
   EXPECT_THAT(serializer.PrependData(&pl_used, data3), IsOk());
   EXPECT_THAT(serializer.GetBytesUsed(&pl_used), Eq(size));
   EXPECT_THAT(serializer.GetData(&pl_used),
               IsOkAndHolds(ElementsAre(data3, data2, data1, data0)));
 
   // The posting list is FULL. Adding another data should fail.
-  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier> data4(
-      /*document_id=*/4, NamespaceFingerprintIdentifier(
-                             /*namespace_id=*/0, /*fingerprint=*/1234));
+  DocumentIdToJoinInfo<NamespaceIdFingerprint> data4(
+      /*document_id=*/4,
+      NamespaceIdFingerprint(/*namespace_id=*/0, /*fingerprint=*/1234));
   EXPECT_THAT(serializer.PrependData(&pl_used, data4),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
 }
 
 TEST(PostingListJoinDataSerializerTest, PrependSmallerDataShouldFail) {
-  PostingListJoinDataSerializer<
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>>
+  PostingListJoinDataSerializer<DocumentIdToJoinInfo<NamespaceIdFingerprint>>
       serializer;
 
-  int size = 4 * sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>);
+  int size = 4 * sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>);
   ICING_ASSERT_OK_AND_ASSIGN(
       PostingListUsed pl_used,
       PostingListUsed::CreateFromUnitializedRegion(&serializer, size));
 
-  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier> data(
+  DocumentIdToJoinInfo<NamespaceIdFingerprint> data(
       /*document_id=*/100,
-      NamespaceFingerprintIdentifier(/*namespace_id=*/1, /*fingerprint=*/2));
-  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier> smaller_data(
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/2));
+  DocumentIdToJoinInfo<NamespaceIdFingerprint> smaller_data(
       /*document_id=*/99,
-      NamespaceFingerprintIdentifier(/*namespace_id=*/1, /*fingerprint=*/2));
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/2));
 
   // NOT_FULL -> NOT_FULL
   ASSERT_THAT(serializer.PrependData(&pl_used, data), IsOk());
@@ -268,8 +248,7 @@
 }
 
 TEST(PostingListJoinDataSerializerTest, PrependDataPostingListUsedMinSize) {
-  PostingListJoinDataSerializer<
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>>
+  PostingListJoinDataSerializer<DocumentIdToJoinInfo<NamespaceIdFingerprint>>
       serializer;
 
   int size = serializer.GetMinPostingListSize();
@@ -282,59 +261,57 @@
   EXPECT_THAT(serializer.GetData(&pl_used), IsOkAndHolds(IsEmpty()));
 
   // Add a data. PL should shift to ALMOST_FULL state
-  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier> data0(
+  DocumentIdToJoinInfo<NamespaceIdFingerprint> data0(
       /*document_id=*/0,
-      NamespaceFingerprintIdentifier(/*namespace_id=*/1, /*fingerprint=*/2));
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/2));
   EXPECT_THAT(serializer.PrependData(&pl_used, data0), IsOk());
   // Size = sizeof(uncompressed data0)
-  int expected_size =
-      sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>);
+  int expected_size = sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>);
   EXPECT_THAT(serializer.GetBytesUsed(&pl_used), Eq(expected_size));
   EXPECT_THAT(serializer.GetData(&pl_used), IsOkAndHolds(ElementsAre(data0)));
 
   // Add another data. PL should shift to FULL state.
-  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier> data1(
+  DocumentIdToJoinInfo<NamespaceIdFingerprint> data1(
       /*document_id=*/1,
-      NamespaceFingerprintIdentifier(/*namespace_id=*/1, /*fingerprint=*/5));
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/5));
   EXPECT_THAT(serializer.PrependData(&pl_used, data1), IsOk());
   // Size = sizeof(uncompressed data1) + sizeof(uncompressed data0)
-  expected_size += sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>);
+  expected_size += sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>);
   EXPECT_THAT(serializer.GetBytesUsed(&pl_used), Eq(expected_size));
   EXPECT_THAT(serializer.GetData(&pl_used),
               IsOkAndHolds(ElementsAre(data1, data0)));
 
   // The posting list is FULL. Adding another data should fail.
-  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier> data2(
-      /*document_id=*/2, NamespaceFingerprintIdentifier(
-                             /*namespace_id=*/1, /*fingerprint=*/10));
+  DocumentIdToJoinInfo<NamespaceIdFingerprint> data2(
+      /*document_id=*/2,
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/10));
   EXPECT_THAT(serializer.PrependData(&pl_used, data2),
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
 }
 
 TEST(PostingListJoinDataSerializerTest, PrependDataArrayDoNotKeepPrepended) {
-  PostingListJoinDataSerializer<
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>>
+  PostingListJoinDataSerializer<DocumentIdToJoinInfo<NamespaceIdFingerprint>>
       serializer;
 
-  int size = 6 * sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>);
+  int size = 6 * sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>);
   ICING_ASSERT_OK_AND_ASSIGN(
       PostingListUsed pl_used,
       PostingListUsed::CreateFromUnitializedRegion(&serializer, size));
 
-  std::vector<DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>> data_in;
-  std::vector<DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>> data_pushed;
+  std::vector<DocumentIdToJoinInfo<NamespaceIdFingerprint>> data_in;
+  std::vector<DocumentIdToJoinInfo<NamespaceIdFingerprint>> data_pushed;
 
   // Add 3 data. The PL is in the empty state and should be able to fit all 3
   // data without issue, transitioning the PL from EMPTY -> NOT_FULL.
-  data_in.push_back(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
+  data_in.push_back(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
       /*document_id=*/0,
-      NamespaceFingerprintIdentifier(/*namespace_id=*/1, /*fingerprint=*/2)));
-  data_in.push_back(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/2)));
+  data_in.push_back(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
       /*document_id=*/1,
-      NamespaceFingerprintIdentifier(/*namespace_id=*/1, /*fingerprint=*/5)));
-  data_in.push_back(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/5)));
+  data_in.push_back(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
       /*document_id=*/2,
-      NamespaceFingerprintIdentifier(/*namespace_id=*/1, /*fingerprint=*/10)));
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/10)));
   EXPECT_THAT(
       serializer.PrependDataArray(&pl_used, data_in.data(), data_in.size(),
                                   /*keep_prepended=*/false),
@@ -342,19 +319,19 @@
   std::move(data_in.begin(), data_in.end(), std::back_inserter(data_pushed));
   EXPECT_THAT(serializer.GetBytesUsed(&pl_used),
               Eq(data_pushed.size() *
-                 sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>)));
+                 sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>)));
   EXPECT_THAT(
       serializer.GetData(&pl_used),
       IsOkAndHolds(ElementsAreArray(data_pushed.rbegin(), data_pushed.rend())));
 
   // Add 2 data. The PL should transition from NOT_FULL to ALMOST_FULL.
   data_in.clear();
-  data_in.push_back(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
+  data_in.push_back(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
       /*document_id=*/3,
-      NamespaceFingerprintIdentifier(/*namespace_id=*/1, /*fingerprint=*/0)));
-  data_in.push_back(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-      /*document_id=*/4, NamespaceFingerprintIdentifier(/*namespace_id=*/0,
-                                                        /*fingerprint=*/1234)));
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/0)));
+  data_in.push_back(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+      /*document_id=*/4,
+      NamespaceIdFingerprint(/*namespace_id=*/0, /*fingerprint=*/1234)));
   EXPECT_THAT(
       serializer.PrependDataArray(&pl_used, data_in.data(), data_in.size(),
                                   /*keep_prepended=*/false),
@@ -362,7 +339,7 @@
   std::move(data_in.begin(), data_in.end(), std::back_inserter(data_pushed));
   EXPECT_THAT(serializer.GetBytesUsed(&pl_used),
               Eq(data_pushed.size() *
-                 sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>)));
+                 sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>)));
   EXPECT_THAT(
       serializer.GetData(&pl_used),
       IsOkAndHolds(ElementsAreArray(data_pushed.rbegin(), data_pushed.rend())));
@@ -370,19 +347,19 @@
   // Add 2 data. The PL should remain ALMOST_FULL since the remaining space can
   // only fit 1 data.
   data_in.clear();
-  data_in.push_back(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-      /*document_id=*/5, NamespaceFingerprintIdentifier(/*namespace_id=*/2,
-                                                        /*fingerprint=*/99)));
-  data_in.push_back(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-      /*document_id=*/6, NamespaceFingerprintIdentifier(/*namespace_id=*/1,
-                                                        /*fingerprint=*/63)));
+  data_in.push_back(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+      /*document_id=*/5,
+      NamespaceIdFingerprint(/*namespace_id=*/2, /*fingerprint=*/99)));
+  data_in.push_back(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+      /*document_id=*/6,
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/63)));
   EXPECT_THAT(
       serializer.PrependDataArray(&pl_used, data_in.data(), data_in.size(),
                                   /*keep_prepended=*/false),
       IsOkAndHolds(0));
   EXPECT_THAT(serializer.GetBytesUsed(&pl_used),
               Eq(data_pushed.size() *
-                 sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>)));
+                 sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>)));
   EXPECT_THAT(
       serializer.GetData(&pl_used),
       IsOkAndHolds(ElementsAreArray(data_pushed.rbegin(), data_pushed.rend())));
@@ -397,36 +374,35 @@
   std::move(data_in.begin(), data_in.end(), std::back_inserter(data_pushed));
   EXPECT_THAT(serializer.GetBytesUsed(&pl_used),
               Eq(data_pushed.size() *
-                 sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>)));
+                 sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>)));
   EXPECT_THAT(
       serializer.GetData(&pl_used),
       IsOkAndHolds(ElementsAreArray(data_pushed.rbegin(), data_pushed.rend())));
 }
 
 TEST(PostingListJoinDataSerializerTest, PrependDataArrayKeepPrepended) {
-  PostingListJoinDataSerializer<
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>>
+  PostingListJoinDataSerializer<DocumentIdToJoinInfo<NamespaceIdFingerprint>>
       serializer;
 
-  int size = 6 * sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>);
+  int size = 6 * sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>);
   ICING_ASSERT_OK_AND_ASSIGN(
       PostingListUsed pl_used,
       PostingListUsed::CreateFromUnitializedRegion(&serializer, size));
 
-  std::vector<DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>> data_in;
-  std::vector<DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>> data_pushed;
+  std::vector<DocumentIdToJoinInfo<NamespaceIdFingerprint>> data_in;
+  std::vector<DocumentIdToJoinInfo<NamespaceIdFingerprint>> data_pushed;
 
   // Add 3 data. The PL is in the empty state and should be able to fit all 3
   // data without issue, transitioning the PL from EMPTY -> NOT_FULL.
-  data_in.push_back(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
+  data_in.push_back(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
       /*document_id=*/0,
-      NamespaceFingerprintIdentifier(/*namespace_id=*/1, /*fingerprint=*/2)));
-  data_in.push_back(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/2)));
+  data_in.push_back(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
       /*document_id=*/1,
-      NamespaceFingerprintIdentifier(/*namespace_id=*/1, /*fingerprint=*/5)));
-  data_in.push_back(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/5)));
+  data_in.push_back(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
       /*document_id=*/2,
-      NamespaceFingerprintIdentifier(/*namespace_id=*/1, /*fingerprint=*/10)));
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/10)));
   EXPECT_THAT(
       serializer.PrependDataArray(&pl_used, data_in.data(), data_in.size(),
                                   /*keep_prepended=*/true),
@@ -434,7 +410,7 @@
   std::move(data_in.begin(), data_in.end(), std::back_inserter(data_pushed));
   EXPECT_THAT(serializer.GetBytesUsed(&pl_used),
               Eq(data_pushed.size() *
-                 sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>)));
+                 sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>)));
   EXPECT_THAT(
       serializer.GetData(&pl_used),
       IsOkAndHolds(ElementsAreArray(data_pushed.rbegin(), data_pushed.rend())));
@@ -442,18 +418,18 @@
   // Add 4 data. The PL should prepend 3 data and transition from NOT_FULL to
   // FULL.
   data_in.clear();
-  data_in.push_back(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
+  data_in.push_back(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
       /*document_id=*/3,
-      NamespaceFingerprintIdentifier(/*namespace_id=*/1, /*fingerprint=*/0)));
-  data_in.push_back(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-      /*document_id=*/4, NamespaceFingerprintIdentifier(/*namespace_id=*/0,
-                                                        /*fingerprint=*/1234)));
-  data_in.push_back(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-      /*document_id=*/5, NamespaceFingerprintIdentifier(/*namespace_id=*/2,
-                                                        /*fingerprint=*/99)));
-  data_in.push_back(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-      /*document_id=*/6, NamespaceFingerprintIdentifier(/*namespace_id=*/1,
-                                                        /*fingerprint=*/63)));
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/0)));
+  data_in.push_back(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+      /*document_id=*/4,
+      NamespaceIdFingerprint(/*namespace_id=*/0, /*fingerprint=*/1234)));
+  data_in.push_back(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+      /*document_id=*/5,
+      NamespaceIdFingerprint(/*namespace_id=*/2, /*fingerprint=*/99)));
+  data_in.push_back(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+      /*document_id=*/6,
+      NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/63)));
   EXPECT_THAT(
       serializer.PrependDataArray(&pl_used, data_in.data(), data_in.size(),
                                   /*keep_prepended=*/true),
@@ -463,15 +439,14 @@
   std::move(data_in.begin(), data_in.end(), std::back_inserter(data_pushed));
   EXPECT_THAT(serializer.GetBytesUsed(&pl_used),
               Eq(data_pushed.size() *
-                 sizeof(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>)));
+                 sizeof(DocumentIdToJoinInfo<NamespaceIdFingerprint>)));
   EXPECT_THAT(
       serializer.GetData(&pl_used),
       IsOkAndHolds(ElementsAreArray(data_pushed.rbegin(), data_pushed.rend())));
 }
 
 TEST(PostingListJoinDataSerializerTest, MoveFrom) {
-  PostingListJoinDataSerializer<
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>>
+  PostingListJoinDataSerializer<DocumentIdToJoinInfo<NamespaceIdFingerprint>>
       serializer;
 
   int size = 3 * serializer.GetMinPostingListSize();
@@ -479,13 +454,13 @@
       PostingListUsed pl_used1,
       PostingListUsed::CreateFromUnitializedRegion(&serializer, size));
 
-  std::vector<DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>> data_arr1 =
-      {DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-           /*document_id=*/0, NamespaceFingerprintIdentifier(
-                                  /*namespace_id=*/1, /*fingerprint=*/2)),
-       DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-           /*document_id=*/1, NamespaceFingerprintIdentifier(
-                                  /*namespace_id=*/1, /*fingerprint=*/5))};
+  std::vector<DocumentIdToJoinInfo<NamespaceIdFingerprint>> data_arr1 = {
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/0,
+          NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/2)),
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/1,
+          NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/5))};
   ASSERT_THAT(
       serializer.PrependDataArray(&pl_used1, data_arr1.data(), data_arr1.size(),
                                   /*keep_prepended=*/false),
@@ -494,21 +469,19 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       PostingListUsed pl_used2,
       PostingListUsed::CreateFromUnitializedRegion(&serializer, size));
-  std::vector<DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>> data_arr2 =
-      {DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-           /*document_id=*/2, NamespaceFingerprintIdentifier(
-                                  /*namespace_id=*/1, /*fingerprint=*/10)),
-       DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-           /*document_id=*/3, NamespaceFingerprintIdentifier(
-                                  /*namespace_id=*/1, /*fingerprint=*/0)),
-       DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-           /*document_id=*/4,
-           NamespaceFingerprintIdentifier(/*namespace_id=*/0,
-                                          /*fingerprint=*/1234)),
-       DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-           /*document_id=*/5,
-           NamespaceFingerprintIdentifier(/*namespace_id=*/2,
-                                          /*fingerprint=*/99))};
+  std::vector<DocumentIdToJoinInfo<NamespaceIdFingerprint>> data_arr2 = {
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/2,
+          NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/10)),
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/3,
+          NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/0)),
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/4,
+          NamespaceIdFingerprint(/*namespace_id=*/0, /*fingerprint=*/1234)),
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/5,
+          NamespaceIdFingerprint(/*namespace_id=*/2, /*fingerprint=*/99))};
   ASSERT_THAT(
       serializer.PrependDataArray(&pl_used2, data_arr2.data(), data_arr2.size(),
                                   /*keep_prepended=*/false),
@@ -523,21 +496,20 @@
 }
 
 TEST(PostingListJoinDataSerializerTest, MoveToNullReturnsFailedPrecondition) {
-  PostingListJoinDataSerializer<
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>>
+  PostingListJoinDataSerializer<DocumentIdToJoinInfo<NamespaceIdFingerprint>>
       serializer;
 
   int size = 3 * serializer.GetMinPostingListSize();
   ICING_ASSERT_OK_AND_ASSIGN(
       PostingListUsed pl_used,
       PostingListUsed::CreateFromUnitializedRegion(&serializer, size));
-  std::vector<DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>> data_arr = {
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-          /*document_id=*/0, NamespaceFingerprintIdentifier(
-                                 /*namespace_id=*/1, /*fingerprint=*/2)),
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-          /*document_id=*/1, NamespaceFingerprintIdentifier(
-                                 /*namespace_id=*/1, /*fingerprint=*/5))};
+  std::vector<DocumentIdToJoinInfo<NamespaceIdFingerprint>> data_arr = {
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/0,
+          NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/2)),
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/1,
+          NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/5))};
   ASSERT_THAT(
       serializer.PrependDataArray(&pl_used, data_arr.data(), data_arr.size(),
                                   /*keep_prepended=*/false),
@@ -557,31 +529,29 @@
 }
 
 TEST(PostingListJoinDataSerializerTest, MoveToPostingListTooSmall) {
-  PostingListJoinDataSerializer<
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>>
+  PostingListJoinDataSerializer<DocumentIdToJoinInfo<NamespaceIdFingerprint>>
       serializer;
 
   int size1 = 3 * serializer.GetMinPostingListSize();
   ICING_ASSERT_OK_AND_ASSIGN(
       PostingListUsed pl_used1,
       PostingListUsed::CreateFromUnitializedRegion(&serializer, size1));
-  std::vector<DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>> data_arr1 =
-      {DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-           /*document_id=*/0, NamespaceFingerprintIdentifier(
-                                  /*namespace_id=*/1, /*fingerprint=*/2)),
-       DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-           /*document_id=*/1, NamespaceFingerprintIdentifier(
-                                  /*namespace_id=*/1, /*fingerprint=*/5)),
-       DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-           /*document_id=*/2, NamespaceFingerprintIdentifier(
-                                  /*namespace_id=*/1, /*fingerprint=*/10)),
-       DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-           /*document_id=*/3, NamespaceFingerprintIdentifier(
-                                  /*namespace_id=*/1, /*fingerprint=*/0)),
-       DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-           /*document_id=*/4,
-           NamespaceFingerprintIdentifier(/*namespace_id=*/0,
-                                          /*fingerprint=*/1234))};
+  std::vector<DocumentIdToJoinInfo<NamespaceIdFingerprint>> data_arr1 = {
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/0,
+          NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/2)),
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/1,
+          NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/5)),
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/2,
+          NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/10)),
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/3,
+          NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/0)),
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/4,
+          NamespaceIdFingerprint(/*namespace_id=*/0, /*fingerprint=*/1234))};
   ASSERT_THAT(
       serializer.PrependDataArray(&pl_used1, data_arr1.data(), data_arr1.size(),
                                   /*keep_prepended=*/false),
@@ -591,10 +561,10 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       PostingListUsed pl_used2,
       PostingListUsed::CreateFromUnitializedRegion(&serializer, size2));
-  std::vector<DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>> data_arr2 =
-      {DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-          /*document_id=*/5, NamespaceFingerprintIdentifier(
-                                 /*namespace_id=*/2, /*fingerprint=*/99))};
+  std::vector<DocumentIdToJoinInfo<NamespaceIdFingerprint>> data_arr2 = {
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/5,
+          NamespaceIdFingerprint(/*namespace_id=*/2, /*fingerprint=*/99))};
   ASSERT_THAT(
       serializer.PrependDataArray(&pl_used2, data_arr2.data(), data_arr2.size(),
                                   /*keep_prepended=*/false),
@@ -611,8 +581,7 @@
 }
 
 TEST(PostingListJoinDataSerializerTest, PopFrontData) {
-  PostingListJoinDataSerializer<
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>>
+  PostingListJoinDataSerializer<DocumentIdToJoinInfo<NamespaceIdFingerprint>>
       serializer;
 
   int size = 2 * serializer.GetMinPostingListSize();
@@ -620,16 +589,16 @@
       PostingListUsed pl_used,
       PostingListUsed::CreateFromUnitializedRegion(&serializer, size));
 
-  std::vector<DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>> data_arr = {
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-          /*document_id=*/0, NamespaceFingerprintIdentifier(
-                                 /*namespace_id=*/1, /*fingerprint=*/2)),
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-          /*document_id=*/1, NamespaceFingerprintIdentifier(
-                                 /*namespace_id=*/1, /*fingerprint=*/5)),
-      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-          /*document_id=*/2, NamespaceFingerprintIdentifier(
-                                 /*namespace_id=*/1, /*fingerprint=*/10))};
+  std::vector<DocumentIdToJoinInfo<NamespaceIdFingerprint>> data_arr = {
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/0,
+          NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/2)),
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/1,
+          NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/5)),
+      DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/2,
+          NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/10))};
   ASSERT_THAT(
       serializer.PrependDataArray(&pl_used, data_arr.data(), data_arr.size(),
                                   /*keep_prepended=*/false),
diff --git a/icing/join/qualified-id-join-index-impl-v1.cc b/icing/join/qualified-id-join-index-impl-v1.cc
index a9be0c1..da45e00 100644
--- a/icing/join/qualified-id-join-index-impl-v1.cc
+++ b/icing/join/qualified-id-join-index-impl-v1.cc
@@ -14,6 +14,7 @@
 
 #include "icing/join/qualified-id-join-index-impl-v1.h"
 
+#include <cstdint>
 #include <cstring>
 #include <memory>
 #include <string>
@@ -29,7 +30,7 @@
 #include "icing/file/file-backed-vector.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/memory-mapped-file.h"
-#include "icing/join/doc-join-info.h"
+#include "icing/join/document-join-id-pair.h"
 #include "icing/join/qualified-id-join-index.h"
 #include "icing/store/document-id.h"
 #include "icing/store/dynamic-trie-key-mapper.h"
@@ -48,10 +49,10 @@
 
 // Set 1M for max # of qualified id entries and 10 bytes for key-value bytes.
 // This will take at most 23 MiB disk space and mmap for persistent hash map.
-static constexpr int32_t kDocJoinInfoMapperMaxNumEntries = 1 << 20;
-static constexpr int32_t kDocJoinInfoMapperAverageKVByteSize = 10;
+static constexpr int32_t kDocumentJoinIdPairMapperMaxNumEntries = 1 << 20;
+static constexpr int32_t kDocumentJoinIdPairMapperAverageKVByteSize = 10;
 
-static constexpr int32_t kDocJoinInfoMapperDynamicTrieMaxSize =
+static constexpr int32_t kDocumentJoinIdPairMapperDynamicTrieMaxSize =
     128 * 1024 * 1024;  // 128 MiB
 
 DocumentId GetNewDocumentId(
@@ -67,7 +68,10 @@
   return absl_ports::StrCat(working_path, "/metadata");
 }
 
-std::string GetDocJoinInfoMapperPath(std::string_view working_path) {
+std::string GetDocumentJoinIdPairMapperPath(std::string_view working_path) {
+  // Use the old directory name "doc_join_info_mapper" to avoid rebuild. We just
+  // changed the class, variable and function name, so there is no need to
+  // change the directory name or rebuild the index.
   return absl_ports::StrCat(working_path, "/doc_join_info_mapper");
 }
 
@@ -85,7 +89,7 @@
                                    bool use_persistent_hash_map) {
   if (!filesystem.FileExists(GetMetadataFilePath(working_path).c_str()) ||
       !filesystem.DirectoryExists(
-          GetDocJoinInfoMapperPath(working_path).c_str()) ||
+          GetDocumentJoinIdPairMapperPath(working_path).c_str()) ||
       !filesystem.FileExists(GetQualifiedIdStoragePath(working_path).c_str())) {
     // Discard working_path if any file/directory is missing, and reinitialize.
     if (filesystem.DirectoryExists(working_path.c_str())) {
@@ -108,12 +112,13 @@
 }
 
 libtextclassifier3::Status QualifiedIdJoinIndexImplV1::Put(
-    const DocJoinInfo& doc_join_info, std::string_view ref_qualified_id_str) {
+    const DocumentJoinIdPair& document_join_id_pair,
+    std::string_view ref_qualified_id_str) {
   SetDirty();
 
-  if (!doc_join_info.is_valid()) {
+  if (!document_join_id_pair.is_valid()) {
     return absl_ports::InvalidArgumentError(
-        "Cannot put data for an invalid DocJoinInfo");
+        "Cannot put data for an invalid DocumentJoinIdPair");
   }
 
   int32_t qualified_id_index = qualified_id_storage_->num_elements();
@@ -125,8 +130,8 @@
   mutable_arr.SetArray(/*idx=*/ref_qualified_id_str.size(), /*arr=*/"\0",
                        /*arr_len=*/1);
 
-  ICING_RETURN_IF_ERROR(doc_join_info_mapper_->Put(
-      encode_util::EncodeIntToCString(doc_join_info.value()),
+  ICING_RETURN_IF_ERROR(document_join_id_pair_mapper_->Put(
+      encode_util::EncodeIntToCString(document_join_id_pair.value()),
       qualified_id_index));
 
   // TODO(b/268521214): add data into delete propagation storage
@@ -135,16 +140,16 @@
 }
 
 libtextclassifier3::StatusOr<std::string_view> QualifiedIdJoinIndexImplV1::Get(
-    const DocJoinInfo& doc_join_info) const {
-  if (!doc_join_info.is_valid()) {
+    const DocumentJoinIdPair& document_join_id_pair) const {
+  if (!document_join_id_pair.is_valid()) {
     return absl_ports::InvalidArgumentError(
-        "Cannot get data for an invalid DocJoinInfo");
+        "Cannot get data for an invalid DocumentJoinIdPair");
   }
 
   ICING_ASSIGN_OR_RETURN(
       int32_t qualified_id_index,
-      doc_join_info_mapper_->Get(
-          encode_util::EncodeIntToCString(doc_join_info.value())));
+      document_join_id_pair_mapper_->Get(
+          encode_util::EncodeIntToCString(document_join_id_pair.value())));
 
   const char* data = qualified_id_storage_->array() + qualified_id_index;
   return std::string_view(data, strlen(data));
@@ -182,7 +187,7 @@
 
   // Destruct current index's storage instances to safely swap directories.
   // TODO(b/268521214): handle delete propagation storage
-  doc_join_info_mapper_.reset();
+  document_join_id_pair_mapper_.reset();
   qualified_id_storage_.reset();
 
   if (!filesystem_.SwapFiles(temp_working_path_ddir.dir().c_str(),
@@ -200,18 +205,19 @@
   }
   if (use_persistent_hash_map_) {
     ICING_ASSIGN_OR_RETURN(
-        doc_join_info_mapper_,
+        document_join_id_pair_mapper_,
         PersistentHashMapKeyMapper<int32_t>::Create(
-            filesystem_, GetDocJoinInfoMapperPath(working_path_),
+            filesystem_, GetDocumentJoinIdPairMapperPath(working_path_),
             pre_mapping_fbv_,
-            /*max_num_entries=*/kDocJoinInfoMapperMaxNumEntries,
-            /*average_kv_byte_size=*/kDocJoinInfoMapperAverageKVByteSize));
+            /*max_num_entries=*/kDocumentJoinIdPairMapperMaxNumEntries,
+            /*average_kv_byte_size=*/
+            kDocumentJoinIdPairMapperAverageKVByteSize));
   } else {
     ICING_ASSIGN_OR_RETURN(
-        doc_join_info_mapper_,
+        document_join_id_pair_mapper_,
         DynamicTrieKeyMapper<int32_t>::Create(
-            filesystem_, GetDocJoinInfoMapperPath(working_path_),
-            kDocJoinInfoMapperDynamicTrieMaxSize));
+            filesystem_, GetDocumentJoinIdPairMapperPath(working_path_),
+            kDocumentJoinIdPairMapperDynamicTrieMaxSize));
   }
 
   ICING_ASSIGN_OR_RETURN(
@@ -228,26 +234,28 @@
 libtextclassifier3::Status QualifiedIdJoinIndexImplV1::Clear() {
   SetDirty();
 
-  doc_join_info_mapper_.reset();
+  document_join_id_pair_mapper_.reset();
   // Discard and reinitialize doc join info mapper.
-  std::string doc_join_info_mapper_path =
-      GetDocJoinInfoMapperPath(working_path_);
+  std::string document_join_id_pair_mapper_path =
+      GetDocumentJoinIdPairMapperPath(working_path_);
   if (use_persistent_hash_map_) {
     ICING_RETURN_IF_ERROR(PersistentHashMapKeyMapper<int32_t>::Delete(
-        filesystem_, doc_join_info_mapper_path));
+        filesystem_, document_join_id_pair_mapper_path));
     ICING_ASSIGN_OR_RETURN(
-        doc_join_info_mapper_,
+        document_join_id_pair_mapper_,
         PersistentHashMapKeyMapper<int32_t>::Create(
-            filesystem_, std::move(doc_join_info_mapper_path), pre_mapping_fbv_,
-            /*max_num_entries=*/kDocJoinInfoMapperMaxNumEntries,
-            /*average_kv_byte_size=*/kDocJoinInfoMapperAverageKVByteSize));
+            filesystem_, std::move(document_join_id_pair_mapper_path),
+            pre_mapping_fbv_,
+            /*max_num_entries=*/kDocumentJoinIdPairMapperMaxNumEntries,
+            /*average_kv_byte_size=*/
+            kDocumentJoinIdPairMapperAverageKVByteSize));
   } else {
     ICING_RETURN_IF_ERROR(DynamicTrieKeyMapper<int32_t>::Delete(
-        filesystem_, doc_join_info_mapper_path));
-    ICING_ASSIGN_OR_RETURN(doc_join_info_mapper_,
+        filesystem_, document_join_id_pair_mapper_path));
+    ICING_ASSIGN_OR_RETURN(document_join_id_pair_mapper_,
                            DynamicTrieKeyMapper<int32_t>::Create(
-                               filesystem_, doc_join_info_mapper_path,
-                               kDocJoinInfoMapperDynamicTrieMaxSize));
+                               filesystem_, document_join_id_pair_mapper_path,
+                               kDocumentJoinIdPairMapperDynamicTrieMaxSize));
   }
 
   // Clear qualified_id_storage_.
@@ -273,22 +281,24 @@
         absl_ports::StrCat("Failed to create directory: ", working_path));
   }
 
-  // Initialize doc_join_info_mapper
-  std::unique_ptr<KeyMapper<int32_t>> doc_join_info_mapper;
+  // Initialize document_join_id_pair_mapper
+  std::unique_ptr<KeyMapper<int32_t>> document_join_id_pair_mapper;
   if (use_persistent_hash_map) {
     // TODO(b/263890397): decide PersistentHashMapKeyMapper size
     ICING_ASSIGN_OR_RETURN(
-        doc_join_info_mapper,
+        document_join_id_pair_mapper,
         PersistentHashMapKeyMapper<int32_t>::Create(
-            filesystem, GetDocJoinInfoMapperPath(working_path), pre_mapping_fbv,
-            /*max_num_entries=*/kDocJoinInfoMapperMaxNumEntries,
-            /*average_kv_byte_size=*/kDocJoinInfoMapperAverageKVByteSize));
+            filesystem, GetDocumentJoinIdPairMapperPath(working_path),
+            pre_mapping_fbv,
+            /*max_num_entries=*/kDocumentJoinIdPairMapperMaxNumEntries,
+            /*average_kv_byte_size=*/
+            kDocumentJoinIdPairMapperAverageKVByteSize));
   } else {
     ICING_ASSIGN_OR_RETURN(
-        doc_join_info_mapper,
+        document_join_id_pair_mapper,
         DynamicTrieKeyMapper<int32_t>::Create(
-            filesystem, GetDocJoinInfoMapperPath(working_path),
-            kDocJoinInfoMapperDynamicTrieMaxSize));
+            filesystem, GetDocumentJoinIdPairMapperPath(working_path),
+            kDocumentJoinIdPairMapperDynamicTrieMaxSize));
   }
 
   // Initialize qualified_id_storage
@@ -305,8 +315,9 @@
       new QualifiedIdJoinIndexImplV1(
           filesystem, std::move(working_path),
           /*metadata_buffer=*/std::make_unique<uint8_t[]>(kMetadataFileSize),
-          std::move(doc_join_info_mapper), std::move(qualified_id_storage),
-          pre_mapping_fbv, use_persistent_hash_map));
+          std::move(document_join_id_pair_mapper),
+          std::move(qualified_id_storage), pre_mapping_fbv,
+          use_persistent_hash_map));
   // Initialize info content.
   new_index->info().magic = Info::kMagic;
   new_index->info().last_added_document_id = kInvalidDocumentId;
@@ -331,9 +342,9 @@
     return absl_ports::InternalError("Fail to read metadata file");
   }
 
-  // Initialize doc_join_info_mapper
+  // Initialize document_join_id_pair_mapper
   bool dynamic_trie_key_mapper_dir_exists = filesystem.DirectoryExists(
-      absl_ports::StrCat(GetDocJoinInfoMapperPath(working_path),
+      absl_ports::StrCat(GetDocumentJoinIdPairMapperPath(working_path),
                          "/key_mapper_dir")
           .c_str());
   if ((use_persistent_hash_map && dynamic_trie_key_mapper_dir_exists) ||
@@ -343,20 +354,22 @@
     return absl_ports::FailedPreconditionError("Key mapper type mismatch");
   }
 
-  std::unique_ptr<KeyMapper<int32_t>> doc_join_info_mapper;
+  std::unique_ptr<KeyMapper<int32_t>> document_join_id_pair_mapper;
   if (use_persistent_hash_map) {
     ICING_ASSIGN_OR_RETURN(
-        doc_join_info_mapper,
+        document_join_id_pair_mapper,
         PersistentHashMapKeyMapper<int32_t>::Create(
-            filesystem, GetDocJoinInfoMapperPath(working_path), pre_mapping_fbv,
-            /*max_num_entries=*/kDocJoinInfoMapperMaxNumEntries,
-            /*average_kv_byte_size=*/kDocJoinInfoMapperAverageKVByteSize));
+            filesystem, GetDocumentJoinIdPairMapperPath(working_path),
+            pre_mapping_fbv,
+            /*max_num_entries=*/kDocumentJoinIdPairMapperMaxNumEntries,
+            /*average_kv_byte_size=*/
+            kDocumentJoinIdPairMapperAverageKVByteSize));
   } else {
     ICING_ASSIGN_OR_RETURN(
-        doc_join_info_mapper,
+        document_join_id_pair_mapper,
         DynamicTrieKeyMapper<int32_t>::Create(
-            filesystem, GetDocJoinInfoMapperPath(working_path),
-            kDocJoinInfoMapperDynamicTrieMaxSize));
+            filesystem, GetDocumentJoinIdPairMapperPath(working_path),
+            kDocumentJoinIdPairMapperDynamicTrieMaxSize));
   }
 
   // Initialize qualified_id_storage
@@ -370,10 +383,11 @@
 
   // Create instance.
   auto type_joinable_index = std::unique_ptr<QualifiedIdJoinIndexImplV1>(
-      new QualifiedIdJoinIndexImplV1(
-          filesystem, std::move(working_path), std::move(metadata_buffer),
-          std::move(doc_join_info_mapper), std::move(qualified_id_storage),
-          pre_mapping_fbv, use_persistent_hash_map));
+      new QualifiedIdJoinIndexImplV1(filesystem, std::move(working_path),
+                                     std::move(metadata_buffer),
+                                     std::move(document_join_id_pair_mapper),
+                                     std::move(qualified_id_storage),
+                                     pre_mapping_fbv, use_persistent_hash_map));
 
   // Initialize existing PersistentStorage. Checksums will be validated.
   ICING_RETURN_IF_ERROR(type_joinable_index->InitializeExistingStorage());
@@ -390,9 +404,9 @@
     const std::vector<DocumentId>& document_id_old_to_new,
     QualifiedIdJoinIndexImplV1* new_index) const {
   std::unique_ptr<KeyMapper<int32_t>::Iterator> iter =
-      doc_join_info_mapper_->GetIterator();
+      document_join_id_pair_mapper_->GetIterator();
   while (iter->Advance()) {
-    DocJoinInfo old_doc_join_info(
+    DocumentJoinIdPair old_document_join_id_pair(
         encode_util::DecodeIntFromCString(iter->GetKey()));
     int32_t qualified_id_index = iter->GetValue();
 
@@ -401,13 +415,13 @@
 
     // Translate to new doc id.
     DocumentId new_document_id = GetNewDocumentId(
-        document_id_old_to_new, old_doc_join_info.document_id());
+        document_id_old_to_new, old_document_join_id_pair.document_id());
 
     if (new_document_id != kInvalidDocumentId) {
-      ICING_RETURN_IF_ERROR(
-          new_index->Put(DocJoinInfo(new_document_id,
-                                     old_doc_join_info.joinable_property_id()),
-                         ref_qualified_id_str));
+      ICING_RETURN_IF_ERROR(new_index->Put(
+          DocumentJoinIdPair(new_document_id,
+                             old_document_join_id_pair.joinable_property_id()),
+          ref_qualified_id_str));
     }
   }
 
@@ -435,7 +449,7 @@
     return libtextclassifier3::Status::OK;
   }
 
-  ICING_RETURN_IF_ERROR(doc_join_info_mapper_->PersistToDisk());
+  ICING_RETURN_IF_ERROR(document_join_id_pair_mapper_->PersistToDisk());
   ICING_RETURN_IF_ERROR(qualified_id_storage_->PersistToDisk());
   is_storage_dirty_ = false;
   return libtextclassifier3::Status::OK;
@@ -469,11 +483,12 @@
     return Crc32(crcs().component_crcs.storages_crc);
   }
 
-  ICING_ASSIGN_OR_RETURN(Crc32 doc_join_info_mapper_crc,
-                         doc_join_info_mapper_->UpdateChecksum());
+  ICING_ASSIGN_OR_RETURN(Crc32 document_join_id_pair_mapper_crc,
+                         document_join_id_pair_mapper_->UpdateChecksum());
   ICING_ASSIGN_OR_RETURN(Crc32 qualified_id_storage_crc,
                          qualified_id_storage_->UpdateChecksum());
-  return Crc32(doc_join_info_mapper_crc.Get() ^ qualified_id_storage_crc.Get());
+  return Crc32(document_join_id_pair_mapper_crc.Get() ^
+               qualified_id_storage_crc.Get());
 }
 
 libtextclassifier3::StatusOr<Crc32>
@@ -491,10 +506,11 @@
     return Crc32(crcs().component_crcs.storages_crc);
   }
 
-  ICING_ASSIGN_OR_RETURN(Crc32 doc_join_info_mapper_crc,
-                         doc_join_info_mapper_->GetChecksum());
+  ICING_ASSIGN_OR_RETURN(Crc32 document_join_id_pair_mapper_crc,
+                         document_join_id_pair_mapper_->GetChecksum());
   Crc32 qualified_id_storage_crc = qualified_id_storage_->GetChecksum();
-  return Crc32(doc_join_info_mapper_crc.Get() ^ qualified_id_storage_crc.Get());
+  return Crc32(document_join_id_pair_mapper_crc.Get() ^
+               qualified_id_storage_crc.Get());
 }
 
 }  // namespace lib
diff --git a/icing/join/qualified-id-join-index-impl-v1.h b/icing/join/qualified-id-join-index-impl-v1.h
index 0d8da56..9a4a6fb 100644
--- a/icing/join/qualified-id-join-index-impl-v1.h
+++ b/icing/join/qualified-id-join-index-impl-v1.h
@@ -28,21 +28,21 @@
 #include "icing/file/file-backed-vector.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/persistent-storage.h"
-#include "icing/join/doc-join-info.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/store/document-filter-data.h"
 #include "icing/store/document-id.h"
 #include "icing/store/key-mapper.h"
-#include "icing/store/namespace-fingerprint-identifier.h"
+#include "icing/store/namespace-id-fingerprint.h"
 #include "icing/store/namespace-id.h"
 #include "icing/util/crc32.h"
 
 namespace icing {
 namespace lib {
 
-// QualifiedIdJoinIndexImplV1: a class to maintain data mapping DocJoinInfo to
-// joinable qualified ids and delete propagation info.
+// QualifiedIdJoinIndexImplV1: a class to maintain data mapping
+// DocumentJoinIdPair to joinable qualified ids and delete propagation info.
 class QualifiedIdJoinIndexImplV1 : public QualifiedIdJoinIndex {
  public:
   struct Info {
@@ -110,11 +110,11 @@
   ~QualifiedIdJoinIndexImplV1() override;
 
   // v2 only API. Returns UNIMPLEMENTED_ERROR.
-  libtextclassifier3::Status Put(SchemaTypeId schema_type_id,
-                                 JoinablePropertyId joinable_property_id,
-                                 DocumentId document_id,
-                                 std::vector<NamespaceFingerprintIdentifier>&&
-                                     ref_namespace_fingerprint_ids) override {
+  libtextclassifier3::Status Put(
+      SchemaTypeId schema_type_id, JoinablePropertyId joinable_property_id,
+      DocumentId document_id,
+      std::vector<NamespaceIdFingerprint>&& ref_namespace_id_uri_fingerprints)
+      override {
     return absl_ports::UnimplementedError("This API is not supported in V1");
   }
 
@@ -125,12 +125,32 @@
     return absl_ports::UnimplementedError("This API is not supported in V1");
   }
 
+  // v3 only API. Returns UNIMPLEMENTED_ERROR.
   libtextclassifier3::Status Put(
-      const DocJoinInfo& doc_join_info,
+      const DocumentJoinIdPair& child_document_join_id_pair,
+      std::vector<DocumentId>&& parent_document_ids) override {
+    return absl_ports::UnimplementedError("This API is not supported in V1");
+  }
+
+  // v3 only API. Returns UNIMPLEMENTED_ERROR.
+  libtextclassifier3::StatusOr<std::vector<DocumentJoinIdPair>> Get(
+      DocumentId parent_document_id) const override {
+    return absl_ports::UnimplementedError("This API is not supported in V1");
+  }
+
+  libtextclassifier3::Status Put(
+      const DocumentJoinIdPair& document_join_id_pair,
       std::string_view ref_qualified_id_str) override;
 
   libtextclassifier3::StatusOr<std::string_view> Get(
-      const DocJoinInfo& doc_join_info) const override;
+      const DocumentJoinIdPair& document_join_id_pair) const override;
+
+  // No-op since v1 stores parent information in raw qualified id string format
+  // and does not require parent migration.
+  libtextclassifier3::Status MigrateParent(
+      DocumentId old_document_id, DocumentId new_document_id) override {
+    return libtextclassifier3::Status::OK;
+  }
 
   libtextclassifier3::Status Optimize(
       const std::vector<DocumentId>& document_id_old_to_new,
@@ -139,9 +159,13 @@
 
   libtextclassifier3::Status Clear() override;
 
-  bool is_v2() const override { return false; }
+  QualifiedIdJoinIndex::Version version() const override {
+    return QualifiedIdJoinIndex::Version::kV1;
+  }
 
-  int32_t size() const override { return doc_join_info_mapper_->num_keys(); }
+  int32_t size() const override {
+    return document_join_id_pair_mapper_->num_keys();
+  }
 
   bool empty() const override { return size() == 0; }
 
@@ -163,12 +187,12 @@
   explicit QualifiedIdJoinIndexImplV1(
       const Filesystem& filesystem, std::string&& working_path,
       std::unique_ptr<uint8_t[]> metadata_buffer,
-      std::unique_ptr<KeyMapper<int32_t>> doc_join_info_mapper,
+      std::unique_ptr<KeyMapper<int32_t>> document_join_id_pair_mapper,
       std::unique_ptr<FileBackedVector<char>> qualified_id_storage,
       bool pre_mapping_fbv, bool use_persistent_hash_map)
       : QualifiedIdJoinIndex(filesystem, std::move(working_path)),
         metadata_buffer_(std::move(metadata_buffer)),
-        doc_join_info_mapper_(std::move(doc_join_info_mapper)),
+        document_join_id_pair_mapper_(std::move(document_join_id_pair_mapper)),
         qualified_id_storage_(std::move(qualified_id_storage)),
         pre_mapping_fbv_(pre_mapping_fbv),
         use_persistent_hash_map_(use_persistent_hash_map),
@@ -246,10 +270,10 @@
   // Metadata buffer
   std::unique_ptr<uint8_t[]> metadata_buffer_;
 
-  // Persistent KeyMapper for mapping (encoded) DocJoinInfo (DocumentId,
+  // Persistent KeyMapper for mapping (encoded) DocumentJoinIdPair (DocumentId,
   // JoinablePropertyId) to another referenced document's qualified id string
   // index in qualified_id_storage_.
-  std::unique_ptr<KeyMapper<int32_t>> doc_join_info_mapper_;
+  std::unique_ptr<KeyMapper<int32_t>> document_join_id_pair_mapper_;
 
   // Storage for qualified id strings.
   std::unique_ptr<FileBackedVector<char>> qualified_id_storage_;
diff --git a/icing/join/qualified-id-join-index-impl-v1_test.cc b/icing/join/qualified-id-join-index-impl-v1_test.cc
index bde9066..5af86fc 100644
--- a/icing/join/qualified-id-join-index-impl-v1_test.cc
+++ b/icing/join/qualified-id-join-index-impl-v1_test.cc
@@ -25,7 +25,7 @@
 #include "icing/file/file-backed-vector.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/persistent-storage.h"
-#include "icing/join/doc-join-info.h"
+#include "icing/join/document-join-id-pair.h"
 #include "icing/store/document-id.h"
 #include "icing/store/dynamic-trie-key-mapper.h"
 #include "icing/store/key-mapper.h"
@@ -158,16 +158,16 @@
                                          param.use_persistent_hash_map));
 
   // Insert some data.
-  ICING_ASSERT_OK(
-      index->Put(DocJoinInfo(/*document_id=*/1, /*joinable_property_id=*/20),
-                 /*ref_qualified_id_str=*/"namespace#uriA"));
+  ICING_ASSERT_OK(index->Put(
+      DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/20),
+      /*ref_qualified_id_str=*/"namespace#uriA"));
   ICING_ASSERT_OK(index->PersistToDisk());
-  ICING_ASSERT_OK(
-      index->Put(DocJoinInfo(/*document_id=*/3, /*joinable_property_id=*/20),
-                 /*ref_qualified_id_str=*/"namespace#uriB"));
-  ICING_ASSERT_OK(
-      index->Put(DocJoinInfo(/*document_id=*/5, /*joinable_property_id=*/20),
-                 /*ref_qualified_id_str=*/"namespace#uriC"));
+  ICING_ASSERT_OK(index->Put(
+      DocumentJoinIdPair(/*document_id=*/3, /*joinable_property_id=*/20),
+      /*ref_qualified_id_str=*/"namespace#uriB"));
+  ICING_ASSERT_OK(index->Put(
+      DocumentJoinIdPair(/*document_id=*/5, /*joinable_property_id=*/20),
+      /*ref_qualified_id_str=*/"namespace#uriC"));
   // GetChecksum should succeed without updating the checksum.
   ICING_EXPECT_OK(index->GetChecksum());
 
@@ -193,15 +193,15 @@
                                          param.use_persistent_hash_map));
 
   // Insert some data.
-  ICING_ASSERT_OK(
-      index1->Put(DocJoinInfo(/*document_id=*/1, /*joinable_property_id=*/20),
-                  /*ref_qualified_id_str=*/"namespace#uriA"));
-  ICING_ASSERT_OK(
-      index1->Put(DocJoinInfo(/*document_id=*/3, /*joinable_property_id=*/20),
-                  /*ref_qualified_id_str=*/"namespace#uriB"));
-  ICING_ASSERT_OK(
-      index1->Put(DocJoinInfo(/*document_id=*/5, /*joinable_property_id=*/20),
-                  /*ref_qualified_id_str=*/"namespace#uriC"));
+  ICING_ASSERT_OK(index1->Put(
+      DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/20),
+      /*ref_qualified_id_str=*/"namespace#uriA"));
+  ICING_ASSERT_OK(index1->Put(
+      DocumentJoinIdPair(/*document_id=*/3, /*joinable_property_id=*/20),
+      /*ref_qualified_id_str=*/"namespace#uriB"));
+  ICING_ASSERT_OK(index1->Put(
+      DocumentJoinIdPair(/*document_id=*/5, /*joinable_property_id=*/20),
+      /*ref_qualified_id_str=*/"namespace#uriC"));
   ASSERT_THAT(index1, Pointee(SizeIs(3)));
 
   // After calling UpdateChecksums, all checksums should be recomputed and
@@ -217,15 +217,15 @@
                                          param.pre_mapping_fbv,
                                          param.use_persistent_hash_map));
   EXPECT_THAT(index2, Pointee(SizeIs(3)));
-  EXPECT_THAT(
-      index2->Get(DocJoinInfo(/*document_id=*/1, /*joinable_property_id=*/20)),
-      IsOkAndHolds(/*ref_qualified_id_str=*/"namespace#uriA"));
-  EXPECT_THAT(
-      index2->Get(DocJoinInfo(/*document_id=*/3, /*joinable_property_id=*/20)),
-      IsOkAndHolds(/*ref_qualified_id_str=*/"namespace#uriB"));
-  EXPECT_THAT(
-      index2->Get(DocJoinInfo(/*document_id=*/5, /*joinable_property_id=*/20)),
-      IsOkAndHolds(/*ref_qualified_id_str=*/"namespace#uriC"));
+  EXPECT_THAT(index2->Get(DocumentJoinIdPair(/*document_id=*/1,
+                                             /*joinable_property_id=*/20)),
+              IsOkAndHolds(/*ref_qualified_id_str=*/"namespace#uriA"));
+  EXPECT_THAT(index2->Get(DocumentJoinIdPair(/*document_id=*/3,
+                                             /*joinable_property_id=*/20)),
+              IsOkAndHolds(/*ref_qualified_id_str=*/"namespace#uriB"));
+  EXPECT_THAT(index2->Get(DocumentJoinIdPair(/*document_id=*/5,
+                                             /*joinable_property_id=*/20)),
+              IsOkAndHolds(/*ref_qualified_id_str=*/"namespace#uriC"));
 }
 
 TEST_P(QualifiedIdJoinIndexImplV1Test,
@@ -240,15 +240,15 @@
                                          param.use_persistent_hash_map));
 
   // Insert some data.
-  ICING_ASSERT_OK(
-      index1->Put(DocJoinInfo(/*document_id=*/1, /*joinable_property_id=*/20),
-                  /*ref_qualified_id_str=*/"namespace#uriA"));
-  ICING_ASSERT_OK(
-      index1->Put(DocJoinInfo(/*document_id=*/3, /*joinable_property_id=*/20),
-                  /*ref_qualified_id_str=*/"namespace#uriB"));
-  ICING_ASSERT_OK(
-      index1->Put(DocJoinInfo(/*document_id=*/5, /*joinable_property_id=*/20),
-                  /*ref_qualified_id_str=*/"namespace#uriC"));
+  ICING_ASSERT_OK(index1->Put(
+      DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/20),
+      /*ref_qualified_id_str=*/"namespace#uriA"));
+  ICING_ASSERT_OK(index1->Put(
+      DocumentJoinIdPair(/*document_id=*/3, /*joinable_property_id=*/20),
+      /*ref_qualified_id_str=*/"namespace#uriB"));
+  ICING_ASSERT_OK(index1->Put(
+      DocumentJoinIdPair(/*document_id=*/5, /*joinable_property_id=*/20),
+      /*ref_qualified_id_str=*/"namespace#uriC"));
   ASSERT_THAT(index1, Pointee(SizeIs(3)));
 
   // After calling PersistToDisk, all checksums should be recomputed and synced
@@ -262,15 +262,15 @@
                                          param.pre_mapping_fbv,
                                          param.use_persistent_hash_map));
   EXPECT_THAT(index2, Pointee(SizeIs(3)));
-  EXPECT_THAT(
-      index2->Get(DocJoinInfo(/*document_id=*/1, /*joinable_property_id=*/20)),
-      IsOkAndHolds(/*ref_qualified_id_str=*/"namespace#uriA"));
-  EXPECT_THAT(
-      index2->Get(DocJoinInfo(/*document_id=*/3, /*joinable_property_id=*/20)),
-      IsOkAndHolds(/*ref_qualified_id_str=*/"namespace#uriB"));
-  EXPECT_THAT(
-      index2->Get(DocJoinInfo(/*document_id=*/5, /*joinable_property_id=*/20)),
-      IsOkAndHolds(/*ref_qualified_id_str=*/"namespace#uriC"));
+  EXPECT_THAT(index2->Get(DocumentJoinIdPair(/*document_id=*/1,
+                                             /*joinable_property_id=*/20)),
+              IsOkAndHolds(/*ref_qualified_id_str=*/"namespace#uriA"));
+  EXPECT_THAT(index2->Get(DocumentJoinIdPair(/*document_id=*/3,
+                                             /*joinable_property_id=*/20)),
+              IsOkAndHolds(/*ref_qualified_id_str=*/"namespace#uriB"));
+  EXPECT_THAT(index2->Get(DocumentJoinIdPair(/*document_id=*/5,
+                                             /*joinable_property_id=*/20)),
+              IsOkAndHolds(/*ref_qualified_id_str=*/"namespace#uriC"));
 }
 
 TEST_P(QualifiedIdJoinIndexImplV1Test,
@@ -286,15 +286,15 @@
                                            param.use_persistent_hash_map));
 
     // Insert some data.
-    ICING_ASSERT_OK(
-        index->Put(DocJoinInfo(/*document_id=*/1, /*joinable_property_id=*/20),
-                   /*ref_qualified_id_str=*/"namespace#uriA"));
-    ICING_ASSERT_OK(
-        index->Put(DocJoinInfo(/*document_id=*/3, /*joinable_property_id=*/20),
-                   /*ref_qualified_id_str=*/"namespace#uriB"));
-    ICING_ASSERT_OK(
-        index->Put(DocJoinInfo(/*document_id=*/5, /*joinable_property_id=*/20),
-                   /*ref_qualified_id_str=*/"namespace#uriC"));
+    ICING_ASSERT_OK(index->Put(
+        DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/20),
+        /*ref_qualified_id_str=*/"namespace#uriA"));
+    ICING_ASSERT_OK(index->Put(
+        DocumentJoinIdPair(/*document_id=*/3, /*joinable_property_id=*/20),
+        /*ref_qualified_id_str=*/"namespace#uriB"));
+    ICING_ASSERT_OK(index->Put(
+        DocumentJoinIdPair(/*document_id=*/5, /*joinable_property_id=*/20),
+        /*ref_qualified_id_str=*/"namespace#uriC"));
     ASSERT_THAT(index, Pointee(SizeIs(3)));
   }
 
@@ -309,14 +309,14 @@
                                            param.pre_mapping_fbv,
                                            param.use_persistent_hash_map));
     EXPECT_THAT(index, Pointee(SizeIs(3)));
-    EXPECT_THAT(index->Get(DocJoinInfo(/*document_id=*/1,
-                                       /*joinable_property_id=*/20)),
+    EXPECT_THAT(index->Get(DocumentJoinIdPair(/*document_id=*/1,
+                                              /*joinable_property_id=*/20)),
                 IsOkAndHolds("namespace#uriA"));
-    EXPECT_THAT(index->Get(DocJoinInfo(/*document_id=*/3,
-                                       /*joinable_property_id=*/20)),
+    EXPECT_THAT(index->Get(DocumentJoinIdPair(/*document_id=*/3,
+                                              /*joinable_property_id=*/20)),
                 IsOkAndHolds("namespace#uriB"));
-    EXPECT_THAT(index->Get(DocJoinInfo(/*document_id=*/5,
-                                       /*joinable_property_id=*/20)),
+    EXPECT_THAT(index->Get(DocumentJoinIdPair(/*document_id=*/5,
+                                              /*joinable_property_id=*/20)),
                 IsOkAndHolds("namespace#uriC"));
   }
 }
@@ -332,9 +332,9 @@
         QualifiedIdJoinIndexImplV1::Create(filesystem_, working_path_,
                                            param.pre_mapping_fbv,
                                            param.use_persistent_hash_map));
-    ICING_ASSERT_OK(
-        index->Put(DocJoinInfo(/*document_id=*/1, /*joinable_property_id=*/20),
-                   /*ref_qualified_id_str=*/"namespace#uriA"));
+    ICING_ASSERT_OK(index->Put(
+        DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/20),
+        /*ref_qualified_id_str=*/"namespace#uriA"));
 
     ICING_ASSERT_OK(index->PersistToDisk());
   }
@@ -389,9 +389,9 @@
         QualifiedIdJoinIndexImplV1::Create(filesystem_, working_path_,
                                            param.pre_mapping_fbv,
                                            param.use_persistent_hash_map));
-    ICING_ASSERT_OK(
-        index->Put(DocJoinInfo(/*document_id=*/1, /*joinable_property_id=*/20),
-                   /*ref_qualified_id_str=*/"namespace#uriA"));
+    ICING_ASSERT_OK(index->Put(
+        DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/20),
+        /*ref_qualified_id_str=*/"namespace#uriA"));
 
     ICING_ASSERT_OK(index->PersistToDisk());
   }
@@ -441,9 +441,9 @@
         QualifiedIdJoinIndexImplV1::Create(filesystem_, working_path_,
                                            param.pre_mapping_fbv,
                                            param.use_persistent_hash_map));
-    ICING_ASSERT_OK(
-        index->Put(DocJoinInfo(/*document_id=*/1, /*joinable_property_id=*/20),
-                   /*ref_qualified_id_str=*/"namespace#uriA"));
+    ICING_ASSERT_OK(index->Put(
+        DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/20),
+        /*ref_qualified_id_str=*/"namespace#uriA"));
 
     ICING_ASSERT_OK(index->PersistToDisk());
   }
@@ -484,7 +484,7 @@
 }
 
 TEST_P(QualifiedIdJoinIndexImplV1Test,
-       InitializeExistingFilesWithCorruptedDocJoinInfoMapperShouldFail) {
+       InitializeExistingFilesWithCorruptedDocumentJoinIdPairMapperShouldFail) {
   const QualifiedIdJoinIndexImplV1TestParam& param = GetParam();
 
   {
@@ -494,9 +494,9 @@
         QualifiedIdJoinIndexImplV1::Create(filesystem_, working_path_,
                                            param.pre_mapping_fbv,
                                            param.use_persistent_hash_map));
-    ICING_ASSERT_OK(
-        index->Put(DocJoinInfo(/*document_id=*/1, /*joinable_property_id=*/20),
-                   /*ref_qualified_id_str=*/"namespace#uriA"));
+    ICING_ASSERT_OK(index->Put(
+        DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/20),
+        /*ref_qualified_id_str=*/"namespace#uriA"));
 
     ICING_ASSERT_OK(index->PersistToDisk());
   }
@@ -544,9 +544,9 @@
         QualifiedIdJoinIndexImplV1::Create(filesystem_, working_path_,
                                            param.pre_mapping_fbv,
                                            param.use_persistent_hash_map));
-    ICING_ASSERT_OK(
-        index->Put(DocJoinInfo(/*document_id=*/1, /*joinable_property_id=*/20),
-                   /*ref_qualified_id_str=*/"namespace#uriA"));
+    ICING_ASSERT_OK(index->Put(
+        DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/20),
+        /*ref_qualified_id_str=*/"namespace#uriA"));
 
     ICING_ASSERT_OK(index->PersistToDisk());
   }
@@ -589,7 +589,7 @@
                                          param.pre_mapping_fbv,
                                          param.use_persistent_hash_map));
 
-  DocJoinInfo default_invalid;
+  DocumentJoinIdPair default_invalid;
   EXPECT_THAT(
       index->Put(default_invalid, /*ref_qualified_id_str=*/"namespace#uriA"),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
@@ -605,7 +605,7 @@
                                          param.pre_mapping_fbv,
                                          param.use_persistent_hash_map));
 
-  DocJoinInfo default_invalid;
+  DocumentJoinIdPair default_invalid;
   EXPECT_THAT(index->Get(default_invalid),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
@@ -613,13 +613,16 @@
 TEST_P(QualifiedIdJoinIndexImplV1Test, PutAndGet) {
   const QualifiedIdJoinIndexImplV1TestParam& param = GetParam();
 
-  DocJoinInfo target_info1(/*document_id=*/1, /*joinable_property_id=*/20);
+  DocumentJoinIdPair target_id_pair1(/*document_id=*/1,
+                                     /*joinable_property_id=*/20);
   std::string_view ref_qualified_id_str_a = "namespace#uriA";
 
-  DocJoinInfo target_info2(/*document_id=*/3, /*joinable_property_id=*/13);
+  DocumentJoinIdPair target_id_pair2(/*document_id=*/3,
+                                     /*joinable_property_id=*/13);
   std::string_view ref_qualified_id_str_b = "namespace#uriB";
 
-  DocJoinInfo target_info3(/*document_id=*/4, /*joinable_property_id=*/4);
+  DocumentJoinIdPair target_id_pair3(/*document_id=*/4,
+                                     /*joinable_property_id=*/4);
   std::string_view ref_qualified_id_str_c = "namespace#uriC";
 
   {
@@ -630,14 +633,17 @@
                                            param.pre_mapping_fbv,
                                            param.use_persistent_hash_map));
 
-    EXPECT_THAT(index->Put(target_info1, ref_qualified_id_str_a), IsOk());
-    EXPECT_THAT(index->Put(target_info2, ref_qualified_id_str_b), IsOk());
-    EXPECT_THAT(index->Put(target_info3, ref_qualified_id_str_c), IsOk());
+    EXPECT_THAT(index->Put(target_id_pair1, ref_qualified_id_str_a), IsOk());
+    EXPECT_THAT(index->Put(target_id_pair2, ref_qualified_id_str_b), IsOk());
+    EXPECT_THAT(index->Put(target_id_pair3, ref_qualified_id_str_c), IsOk());
     EXPECT_THAT(index, Pointee(SizeIs(3)));
 
-    EXPECT_THAT(index->Get(target_info1), IsOkAndHolds(ref_qualified_id_str_a));
-    EXPECT_THAT(index->Get(target_info2), IsOkAndHolds(ref_qualified_id_str_b));
-    EXPECT_THAT(index->Get(target_info3), IsOkAndHolds(ref_qualified_id_str_c));
+    EXPECT_THAT(index->Get(target_id_pair1),
+                IsOkAndHolds(ref_qualified_id_str_a));
+    EXPECT_THAT(index->Get(target_id_pair2),
+                IsOkAndHolds(ref_qualified_id_str_b));
+    EXPECT_THAT(index->Get(target_id_pair3),
+                IsOkAndHolds(ref_qualified_id_str_c));
 
     ICING_ASSERT_OK(index->PersistToDisk());
   }
@@ -649,15 +655,19 @@
                                          param.pre_mapping_fbv,
                                          param.use_persistent_hash_map));
   EXPECT_THAT(index, Pointee(SizeIs(3)));
-  EXPECT_THAT(index->Get(target_info1), IsOkAndHolds(ref_qualified_id_str_a));
-  EXPECT_THAT(index->Get(target_info2), IsOkAndHolds(ref_qualified_id_str_b));
-  EXPECT_THAT(index->Get(target_info3), IsOkAndHolds(ref_qualified_id_str_c));
+  EXPECT_THAT(index->Get(target_id_pair1),
+              IsOkAndHolds(ref_qualified_id_str_a));
+  EXPECT_THAT(index->Get(target_id_pair2),
+              IsOkAndHolds(ref_qualified_id_str_b));
+  EXPECT_THAT(index->Get(target_id_pair3),
+              IsOkAndHolds(ref_qualified_id_str_c));
 }
 
 TEST_P(QualifiedIdJoinIndexImplV1Test, GetShouldReturnNotFoundErrorIfNotExist) {
   const QualifiedIdJoinIndexImplV1TestParam& param = GetParam();
 
-  DocJoinInfo target_info(/*document_id=*/1, /*joinable_property_id=*/20);
+  DocumentJoinIdPair target_id_pair(/*document_id=*/1,
+                                    /*joinable_property_id=*/20);
   std::string_view ref_qualified_id_str = "namespace#uriA";
 
   // Create new qualified id join index
@@ -668,16 +678,16 @@
                                          param.use_persistent_hash_map));
 
   // Verify entry is not found in the beginning.
-  EXPECT_THAT(index->Get(target_info),
+  EXPECT_THAT(index->Get(target_id_pair),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
-  ICING_ASSERT_OK(index->Put(target_info, ref_qualified_id_str));
-  ASSERT_THAT(index->Get(target_info), IsOkAndHolds(ref_qualified_id_str));
+  ICING_ASSERT_OK(index->Put(target_id_pair, ref_qualified_id_str));
+  ASSERT_THAT(index->Get(target_id_pair), IsOkAndHolds(ref_qualified_id_str));
 
   // Get another non-existing entry. This should get NOT_FOUND_ERROR.
-  DocJoinInfo another_target_info(/*document_id=*/2,
-                                  /*joinable_property_id=*/20);
-  EXPECT_THAT(index->Get(another_target_info),
+  DocumentJoinIdPair another_target_id_pair(/*document_id=*/2,
+                                            /*joinable_property_id=*/20);
+  EXPECT_THAT(index->Get(another_target_id_pair),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 }
 
@@ -732,21 +742,21 @@
                                          param.pre_mapping_fbv,
                                          param.use_persistent_hash_map));
 
-  ICING_ASSERT_OK(
-      index->Put(DocJoinInfo(/*document_id=*/3, /*joinable_property_id=*/10),
-                 /*ref_qualified_id_str=*/"namespace#uriA"));
-  ICING_ASSERT_OK(
-      index->Put(DocJoinInfo(/*document_id=*/5, /*joinable_property_id=*/3),
-                 /*ref_qualified_id_str=*/"namespace#uriA"));
-  ICING_ASSERT_OK(
-      index->Put(DocJoinInfo(/*document_id=*/8, /*joinable_property_id=*/9),
-                 /*ref_qualified_id_str=*/"namespace#uriB"));
-  ICING_ASSERT_OK(
-      index->Put(DocJoinInfo(/*document_id=*/13, /*joinable_property_id=*/4),
-                 /*ref_qualified_id_str=*/"namespace#uriC"));
-  ICING_ASSERT_OK(
-      index->Put(DocJoinInfo(/*document_id=*/21, /*joinable_property_id=*/12),
-                 /*ref_qualified_id_str=*/"namespace#uriC"));
+  ICING_ASSERT_OK(index->Put(
+      DocumentJoinIdPair(/*document_id=*/3, /*joinable_property_id=*/10),
+      /*ref_qualified_id_str=*/"namespace#uriA"));
+  ICING_ASSERT_OK(index->Put(
+      DocumentJoinIdPair(/*document_id=*/5, /*joinable_property_id=*/3),
+      /*ref_qualified_id_str=*/"namespace#uriA"));
+  ICING_ASSERT_OK(index->Put(
+      DocumentJoinIdPair(/*document_id=*/8, /*joinable_property_id=*/9),
+      /*ref_qualified_id_str=*/"namespace#uriB"));
+  ICING_ASSERT_OK(index->Put(
+      DocumentJoinIdPair(/*document_id=*/13, /*joinable_property_id=*/4),
+      /*ref_qualified_id_str=*/"namespace#uriC"));
+  ICING_ASSERT_OK(index->Put(
+      DocumentJoinIdPair(/*document_id=*/21, /*joinable_property_id=*/12),
+      /*ref_qualified_id_str=*/"namespace#uriC"));
   index->set_last_added_document_id(21);
 
   ASSERT_THAT(index, Pointee(SizeIs(5)));
@@ -769,39 +779,40 @@
   // (old_doc_id=3, joinable_property_id=10), which is now (doc_id=0,
   // joinable_property_id=10), has referenced qualified id str =
   // "namespace#uriA".
-  EXPECT_THAT(
-      index->Get(DocJoinInfo(/*document_id=*/0, /*joinable_property_id=*/10)),
-      IsOkAndHolds("namespace#uriA"));
+  EXPECT_THAT(index->Get(DocumentJoinIdPair(/*document_id=*/0,
+                                            /*joinable_property_id=*/10)),
+              IsOkAndHolds("namespace#uriA"));
 
   // (old_doc_id=5, joinable_property_id=3) and (old_doc_id=8,
   // joinable_property_id=9) are now not found since we've deleted old_doc_id =
   // 5, 8. It is not testable via Get() because there is no valid doc_id mapping
-  // for old_doc_id = 5, 8 and we cannot generate a valid DocJoinInfo for it.
+  // for old_doc_id = 5, 8 and we cannot generate a valid DocumentJoinIdPair for
+  // it.
 
   // (old_doc_id=13, joinable_property_id=4), which is now (doc_id=1,
   // joinable_property_id=4), has referenced qualified id str =
   // "namespace#uriC".
-  EXPECT_THAT(
-      index->Get(DocJoinInfo(/*document_id=*/1, /*joinable_property_id=*/4)),
-      IsOkAndHolds("namespace#uriC"));
+  EXPECT_THAT(index->Get(DocumentJoinIdPair(/*document_id=*/1,
+                                            /*joinable_property_id=*/4)),
+              IsOkAndHolds("namespace#uriC"));
 
   // (old_doc_id=21, joinable_property_id=12), which is now (doc_id=2,
   // joinable_property_id=12), has referenced qualified id str =
   // "namespace#uriC".
-  EXPECT_THAT(
-      index->Get(DocJoinInfo(/*document_id=*/2, /*joinable_property_id=*/12)),
-      IsOkAndHolds("namespace#uriC"));
+  EXPECT_THAT(index->Get(DocumentJoinIdPair(/*document_id=*/2,
+                                            /*joinable_property_id=*/12)),
+              IsOkAndHolds("namespace#uriC"));
 
   // Joinable index should be able to work normally after Optimize().
-  ICING_ASSERT_OK(
-      index->Put(DocJoinInfo(/*document_id=*/99, /*joinable_property_id=*/2),
-                 /*ref_qualified_id_str=*/"namespace#uriD"));
+  ICING_ASSERT_OK(index->Put(
+      DocumentJoinIdPair(/*document_id=*/99, /*joinable_property_id=*/2),
+      /*ref_qualified_id_str=*/"namespace#uriD"));
   index->set_last_added_document_id(99);
 
   EXPECT_THAT(index, Pointee(SizeIs(4)));
   EXPECT_THAT(index->last_added_document_id(), Eq(99));
-  EXPECT_THAT(index->Get(DocJoinInfo(/*document_id=*/99,
-                                     /*joinable_property_id=*/2)),
+  EXPECT_THAT(index->Get(DocumentJoinIdPair(/*document_id=*/99,
+                                            /*joinable_property_id=*/2)),
               IsOkAndHolds("namespace#uriD"));
 }
 
@@ -814,9 +825,9 @@
                                          param.pre_mapping_fbv,
                                          param.use_persistent_hash_map));
 
-  ICING_ASSERT_OK(
-      index->Put(DocJoinInfo(/*document_id=*/99, /*joinable_property_id=*/10),
-                 /*ref_qualified_id_str=*/"namespace#uriA"));
+  ICING_ASSERT_OK(index->Put(
+      DocumentJoinIdPair(/*document_id=*/99, /*joinable_property_id=*/10),
+      /*ref_qualified_id_str=*/"namespace#uriA"));
   index->set_last_added_document_id(99);
 
   // Create document_id_old_to_new with size = 1. Optimize should handle out of
@@ -843,21 +854,21 @@
                                          param.pre_mapping_fbv,
                                          param.use_persistent_hash_map));
 
-  ICING_ASSERT_OK(
-      index->Put(DocJoinInfo(/*document_id=*/3, /*joinable_property_id=*/10),
-                 /*ref_qualified_id_str=*/"namespace#uriA"));
-  ICING_ASSERT_OK(
-      index->Put(DocJoinInfo(/*document_id=*/5, /*joinable_property_id=*/3),
-                 /*ref_qualified_id_str=*/"namespace#uriA"));
-  ICING_ASSERT_OK(
-      index->Put(DocJoinInfo(/*document_id=*/8, /*joinable_property_id=*/9),
-                 /*ref_qualified_id_str=*/"namespace#uriB"));
-  ICING_ASSERT_OK(
-      index->Put(DocJoinInfo(/*document_id=*/13, /*joinable_property_id=*/4),
-                 /*ref_qualified_id_str=*/"namespace#uriC"));
-  ICING_ASSERT_OK(
-      index->Put(DocJoinInfo(/*document_id=*/21, /*joinable_property_id=*/12),
-                 /*ref_qualified_id_str=*/"namespace#uriC"));
+  ICING_ASSERT_OK(index->Put(
+      DocumentJoinIdPair(/*document_id=*/3, /*joinable_property_id=*/10),
+      /*ref_qualified_id_str=*/"namespace#uriA"));
+  ICING_ASSERT_OK(index->Put(
+      DocumentJoinIdPair(/*document_id=*/5, /*joinable_property_id=*/3),
+      /*ref_qualified_id_str=*/"namespace#uriA"));
+  ICING_ASSERT_OK(index->Put(
+      DocumentJoinIdPair(/*document_id=*/8, /*joinable_property_id=*/9),
+      /*ref_qualified_id_str=*/"namespace#uriB"));
+  ICING_ASSERT_OK(index->Put(
+      DocumentJoinIdPair(/*document_id=*/13, /*joinable_property_id=*/4),
+      /*ref_qualified_id_str=*/"namespace#uriC"));
+  ICING_ASSERT_OK(index->Put(
+      DocumentJoinIdPair(/*document_id=*/21, /*joinable_property_id=*/12),
+      /*ref_qualified_id_str=*/"namespace#uriC"));
   index->set_last_added_document_id(21);
 
   // Delete all documents.
@@ -876,9 +887,12 @@
 TEST_P(QualifiedIdJoinIndexImplV1Test, Clear) {
   const QualifiedIdJoinIndexImplV1TestParam& param = GetParam();
 
-  DocJoinInfo target_info1(/*document_id=*/1, /*joinable_property_id=*/20);
-  DocJoinInfo target_info2(/*document_id=*/3, /*joinable_property_id=*/5);
-  DocJoinInfo target_info3(/*document_id=*/6, /*joinable_property_id=*/13);
+  DocumentJoinIdPair target_id_pair1(/*document_id=*/1,
+                                     /*joinable_property_id=*/20);
+  DocumentJoinIdPair target_id_pair2(/*document_id=*/3,
+                                     /*joinable_property_id=*/5);
+  DocumentJoinIdPair target_id_pair3(/*document_id=*/6,
+                                     /*joinable_property_id=*/13);
 
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -887,11 +901,11 @@
                                          param.pre_mapping_fbv,
                                          param.use_persistent_hash_map));
   ICING_ASSERT_OK(
-      index->Put(target_info1, /*ref_qualified_id_str=*/"namespace#uriA"));
+      index->Put(target_id_pair1, /*ref_qualified_id_str=*/"namespace#uriA"));
   ICING_ASSERT_OK(
-      index->Put(target_info2, /*ref_qualified_id_str=*/"namespace#uriB"));
+      index->Put(target_id_pair2, /*ref_qualified_id_str=*/"namespace#uriB"));
   ICING_ASSERT_OK(
-      index->Put(target_info3, /*ref_qualified_id_str=*/"namespace#uriC"));
+      index->Put(target_id_pair3, /*ref_qualified_id_str=*/"namespace#uriC"));
   ASSERT_THAT(index, Pointee(SizeIs(3)));
   index->set_last_added_document_id(6);
   ASSERT_THAT(index->last_added_document_id(), Eq(6));
@@ -901,21 +915,22 @@
   EXPECT_THAT(index->Clear(), IsOk());
   EXPECT_THAT(index, Pointee(IsEmpty()));
   EXPECT_THAT(index->last_added_document_id(), Eq(kInvalidDocumentId));
-  EXPECT_THAT(index->Get(target_info1),
+  EXPECT_THAT(index->Get(target_id_pair1),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
-  EXPECT_THAT(index->Get(target_info2),
+  EXPECT_THAT(index->Get(target_id_pair2),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
-  EXPECT_THAT(index->Get(target_info3),
+  EXPECT_THAT(index->Get(target_id_pair3),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
   // Join index should be able to work normally after Clear().
-  DocJoinInfo target_info4(/*document_id=*/2, /*joinable_property_id=*/19);
+  DocumentJoinIdPair target_id_pair4(/*document_id=*/2,
+                                     /*joinable_property_id=*/19);
   ICING_ASSERT_OK(
-      index->Put(target_info4, /*ref_qualified_id_str=*/"namespace#uriD"));
+      index->Put(target_id_pair4, /*ref_qualified_id_str=*/"namespace#uriD"));
   index->set_last_added_document_id(2);
 
   EXPECT_THAT(index->last_added_document_id(), Eq(2));
-  EXPECT_THAT(index->Get(target_info4), IsOkAndHolds("namespace#uriD"));
+  EXPECT_THAT(index->Get(target_id_pair4), IsOkAndHolds("namespace#uriD"));
 
   ICING_ASSERT_OK(index->PersistToDisk());
   index.reset();
@@ -926,13 +941,13 @@
                                                 param.pre_mapping_fbv,
                                                 param.use_persistent_hash_map));
   EXPECT_THAT(index->last_added_document_id(), Eq(2));
-  EXPECT_THAT(index->Get(target_info1),
+  EXPECT_THAT(index->Get(target_id_pair1),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
-  EXPECT_THAT(index->Get(target_info2),
+  EXPECT_THAT(index->Get(target_id_pair2),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
-  EXPECT_THAT(index->Get(target_info3),
+  EXPECT_THAT(index->Get(target_id_pair3),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
-  EXPECT_THAT(index->Get(target_info4), IsOkAndHolds("namespace#uriD"));
+  EXPECT_THAT(index->Get(target_id_pair4), IsOkAndHolds("namespace#uriD"));
 }
 
 TEST_P(QualifiedIdJoinIndexImplV1Test, SwitchKeyMapperTypeShouldReturnError) {
@@ -945,9 +960,9 @@
         QualifiedIdJoinIndexImplV1::Create(filesystem_, working_path_,
                                            param.pre_mapping_fbv,
                                            param.use_persistent_hash_map));
-    ICING_ASSERT_OK(
-        index->Put(DocJoinInfo(/*document_id=*/1, /*joinable_property_id=*/20),
-                   /*ref_qualified_id_str=*/"namespace#uriA"));
+    ICING_ASSERT_OK(index->Put(
+        DocumentJoinIdPair(/*document_id=*/1, /*joinable_property_id=*/20),
+        /*ref_qualified_id_str=*/"namespace#uriA"));
 
     ICING_ASSERT_OK(index->PersistToDisk());
   }
diff --git a/icing/join/qualified-id-join-index-impl-v2.cc b/icing/join/qualified-id-join-index-impl-v2.cc
index 04a535c..0402be2 100644
--- a/icing/join/qualified-id-join-index-impl-v2.cc
+++ b/icing/join/qualified-id-join-index-impl-v2.cc
@@ -39,7 +39,7 @@
 #include "icing/store/document-filter-data.h"
 #include "icing/store/document-id.h"
 #include "icing/store/key-mapper.h"
-#include "icing/store/namespace-fingerprint-identifier.h"
+#include "icing/store/namespace-id-fingerprint.h"
 #include "icing/store/namespace-id.h"
 #include "icing/store/persistent-hash-map-key-mapper.h"
 #include "icing/util/crc32.h"
@@ -62,7 +62,7 @@
 inline DocumentId GetNewDocumentId(
     const std::vector<DocumentId>& document_id_old_to_new,
     DocumentId old_document_id) {
-  if (old_document_id >= document_id_old_to_new.size()) {
+  if (old_document_id < 0 || old_document_id >= document_id_old_to_new.size()) {
     return kInvalidDocumentId;
   }
   return document_id_old_to_new[old_document_id];
@@ -71,7 +71,7 @@
 inline NamespaceId GetNewNamespaceId(
     const std::vector<NamespaceId>& namespace_id_old_to_new,
     NamespaceId namespace_id) {
-  if (namespace_id >= namespace_id_old_to_new.size()) {
+  if (namespace_id < 0 || namespace_id >= namespace_id_old_to_new.size()) {
     return kInvalidNamespaceId;
   }
   return namespace_id_old_to_new[namespace_id];
@@ -224,17 +224,16 @@
 libtextclassifier3::Status QualifiedIdJoinIndexImplV2::Put(
     SchemaTypeId schema_type_id, JoinablePropertyId joinable_property_id,
     DocumentId document_id,
-    std::vector<NamespaceFingerprintIdentifier>&&
-        ref_namespace_fingerprint_ids) {
-  std::sort(ref_namespace_fingerprint_ids.begin(),
-            ref_namespace_fingerprint_ids.end());
+    std::vector<NamespaceIdFingerprint>&& ref_namespace_id_uri_fingerprints) {
+  std::sort(ref_namespace_id_uri_fingerprints.begin(),
+            ref_namespace_id_uri_fingerprints.end());
 
   // Dedupe.
-  auto last = std::unique(ref_namespace_fingerprint_ids.begin(),
-                          ref_namespace_fingerprint_ids.end());
-  ref_namespace_fingerprint_ids.erase(last,
-                                      ref_namespace_fingerprint_ids.end());
-  if (ref_namespace_fingerprint_ids.empty()) {
+  auto last = std::unique(ref_namespace_id_uri_fingerprints.begin(),
+                          ref_namespace_id_uri_fingerprints.end());
+  ref_namespace_id_uri_fingerprints.erase(
+      last, ref_namespace_id_uri_fingerprints.end());
+  if (ref_namespace_id_uri_fingerprints.empty()) {
     return libtextclassifier3::Status::OK;
   }
 
@@ -262,11 +261,11 @@
   }
 
   // Prepend join data into posting list.
-  for (const NamespaceFingerprintIdentifier& ref_namespace_fingerprint_id :
-       ref_namespace_fingerprint_ids) {
-    ICING_RETURN_IF_ERROR(pl_accessor->PrependData(
-        DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-            document_id, ref_namespace_fingerprint_id)));
+  for (const NamespaceIdFingerprint& ref_namespace_id_uri_fingerprint :
+       ref_namespace_id_uri_fingerprints) {
+    ICING_RETURN_IF_ERROR(
+        pl_accessor->PrependData(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+            document_id, ref_namespace_id_uri_fingerprint)));
   }
 
   // Finalize the posting list and update mapper.
@@ -282,7 +281,7 @@
       encoded_schema_type_joinable_property_id_str, result.id));
 
   // Update info.
-  info().num_data += ref_namespace_fingerprint_ids.size();
+  info().num_data += ref_namespace_id_uri_fingerprints.size();
 
   return libtextclassifier3::Status::OK;
 }
@@ -565,9 +564,9 @@
           // We can reuse the fingerprint from old_join_data, since document uri
           // (and its fingerprint) will never change.
           new_join_data_vec.push_back(JoinDataType(
-              new_document_id, NamespaceFingerprintIdentifier(
-                                   new_ref_namespace_id,
-                                   old_join_data.join_info().fingerprint())));
+              new_document_id,
+              NamespaceIdFingerprint(new_ref_namespace_id,
+                                     old_join_data.join_info().fingerprint())));
         }
       }
       ICING_ASSIGN_OR_RETURN(batch_old_join_data,
diff --git a/icing/join/qualified-id-join-index-impl-v2.h b/icing/join/qualified-id-join-index-impl-v2.h
index d45ca41..5b402cb 100644
--- a/icing/join/qualified-id-join-index-impl-v2.h
+++ b/icing/join/qualified-id-join-index-impl-v2.h
@@ -29,8 +29,8 @@
 #include "icing/file/persistent-storage.h"
 #include "icing/file/posting_list/flash-index-storage.h"
 #include "icing/file/posting_list/posting-list-identifier.h"
-#include "icing/join/doc-join-info.h"
 #include "icing/join/document-id-to-join-info.h"
+#include "icing/join/document-join-id-pair.h"
 #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"
@@ -38,7 +38,7 @@
 #include "icing/store/document-filter-data.h"
 #include "icing/store/document-id.h"
 #include "icing/store/key-mapper.h"
-#include "icing/store/namespace-fingerprint-identifier.h"
+#include "icing/store/namespace-id-fingerprint.h"
 #include "icing/store/namespace-id.h"
 #include "icing/util/crc32.h"
 
@@ -46,11 +46,11 @@
 namespace lib {
 
 // QualifiedIdJoinIndexImplV2: a class to maintain join data (DocumentId to
-// referenced NamespaceFingerprintIdentifier). It stores join data in posting
-// lists and bucketizes them by (schema_type_id, joinable_property_id).
+// referenced NamespaceIdFingerprint). It stores join data in posting lists
+// and bucketizes them by (schema_type_id, joinable_property_id).
 class QualifiedIdJoinIndexImplV2 : public QualifiedIdJoinIndex {
  public:
-  using JoinDataType = DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>;
+  using JoinDataType = DocumentIdToJoinInfo<NamespaceIdFingerprint>;
 
   class JoinDataIterator : public JoinDataIteratorBase {
    public:
@@ -109,10 +109,10 @@
       WorkingPathType::kDirectory;
 
   // Creates a QualifiedIdJoinIndexImplV2 instance to store join data
-  // (DocumentId to referenced NamespaceFingerPrintIdentifier) for future
-  // joining search. If any of the underlying file is missing, then delete the
-  // whole working_path and (re)initialize with new ones. Otherwise initialize
-  // and create the instance by existing files.
+  // (DocumentId to referenced NamespaceIdFingerprint) for future joining
+  // search. If any of the underlying file is missing, then delete the whole
+  // working_path and (re)initialize with new ones. Otherwise initialize and
+  // create the instance by existing files.
   //
   // filesystem: Object to make system level calls
   // working_path: Specifies the working path for PersistentStorage.
@@ -152,27 +152,47 @@
 
   // v1 only API. Returns UNIMPLEMENTED_ERROR.
   libtextclassifier3::Status Put(
-      const DocJoinInfo& doc_join_info,
+      const DocumentJoinIdPair& document_join_id_pair,
       std::string_view ref_qualified_id_str) override {
     return absl_ports::UnimplementedError("This API is not supported in V2");
   }
 
   // v1 only API. Returns UNIMPLEMENTED_ERROR.
   libtextclassifier3::StatusOr<std::string_view> Get(
-      const DocJoinInfo& doc_join_info) const override {
+      const DocumentJoinIdPair& document_join_id_pair) const override {
     return absl_ports::UnimplementedError("This API is not supported in V2");
   }
 
-  libtextclassifier3::Status Put(SchemaTypeId schema_type_id,
-                                 JoinablePropertyId joinable_property_id,
-                                 DocumentId document_id,
-                                 std::vector<NamespaceFingerprintIdentifier>&&
-                                     ref_namespace_fingerprint_ids) override;
+  // v3 only API. Returns UNIMPLEMENTED_ERROR.
+  libtextclassifier3::Status Put(
+      const DocumentJoinIdPair& child_document_join_id_pair,
+      std::vector<DocumentId>&& parent_document_ids) override {
+    return absl_ports::UnimplementedError("This API is not supported in V2");
+  }
+
+  // v3 only API. Returns UNIMPLEMENTED_ERROR.
+  libtextclassifier3::StatusOr<std::vector<DocumentJoinIdPair>> Get(
+      DocumentId parent_document_id) const override {
+    return absl_ports::UnimplementedError("This API is not supported in V2");
+  }
+
+  libtextclassifier3::Status Put(
+      SchemaTypeId schema_type_id, JoinablePropertyId joinable_property_id,
+      DocumentId document_id,
+      std::vector<NamespaceIdFingerprint>&& ref_namespace_id_uri_fingerprints)
+      override;
 
   libtextclassifier3::StatusOr<std::unique_ptr<JoinDataIteratorBase>>
   GetIterator(SchemaTypeId schema_type_id,
               JoinablePropertyId joinable_property_id) const override;
 
+  // No-op since v2 stores parent information in (namespace_id,
+  // fingerprint(uri)) format and does not require parent migration.
+  libtextclassifier3::Status MigrateParent(
+      DocumentId old_document_id, DocumentId new_document_id) override {
+    return libtextclassifier3::Status::OK;
+  }
+
   libtextclassifier3::Status Optimize(
       const std::vector<DocumentId>& document_id_old_to_new,
       const std::vector<NamespaceId>& namespace_id_old_to_new,
@@ -180,7 +200,9 @@
 
   libtextclassifier3::Status Clear() override;
 
-  bool is_v2() const override { return true; }
+  QualifiedIdJoinIndex::Version version() const override {
+    return QualifiedIdJoinIndex::Version::kV2;
+  }
 
   int32_t size() const override { return info().num_data; }
 
@@ -296,7 +318,7 @@
       schema_joinable_id_to_posting_list_mapper_;
 
   // Posting list related members. Use posting list to store join data
-  // (document id to referenced NamespaceFingerprintIdentifier).
+  // (document id to referenced NamespaceIdFingerprint).
   std::unique_ptr<PostingListJoinDataSerializer<JoinDataType>>
       posting_list_serializer_;
   std::unique_ptr<FlashIndexStorage> flash_index_storage_;
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 d1d1b69..fdfa8bc 100644
--- a/icing/join/qualified-id-join-index-impl-v2_test.cc
+++ b/icing/join/qualified-id-join-index-impl-v2_test.cc
@@ -36,7 +36,7 @@
 #include "icing/store/document-filter-data.h"
 #include "icing/store/document-id.h"
 #include "icing/store/key-mapper.h"
-#include "icing/store/namespace-fingerprint-identifier.h"
+#include "icing/store/namespace-id-fingerprint.h"
 #include "icing/store/namespace-id.h"
 #include "icing/store/persistent-hash-map-key-mapper.h"
 #include "icing/testing/common-matchers.h"
@@ -161,10 +161,10 @@
 
 TEST_F(QualifiedIdJoinIndexImplV2Test,
        InitializationShouldFailWithoutPersistToDiskOrDestruction) {
-  NamespaceFingerprintIdentifier id1(/*namespace_id=*/1, /*fingerprint=*/12);
-  NamespaceFingerprintIdentifier id2(/*namespace_id=*/1, /*fingerprint=*/34);
-  NamespaceFingerprintIdentifier id3(/*namespace_id=*/1, /*fingerprint=*/56);
-  NamespaceFingerprintIdentifier id4(/*namespace_id=*/1, /*fingerprint=*/78);
+  NamespaceIdFingerprint id1(/*namespace_id=*/1, /*fingerprint=*/12);
+  NamespaceIdFingerprint id2(/*namespace_id=*/1, /*fingerprint=*/34);
+  NamespaceIdFingerprint id3(/*namespace_id=*/1, /*fingerprint=*/56);
+  NamespaceIdFingerprint id4(/*namespace_id=*/1, /*fingerprint=*/78);
 
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -175,14 +175,14 @@
   // Insert some data.
   ICING_ASSERT_OK(index->Put(
       /*schema_type_id=*/2, /*joinable_property_id=*/1, /*document_id=*/5,
-      /*ref_namespace_fingerprint_ids=*/{id2, id1}));
+      /*ref_namespace_id_uri_fingerprints=*/{id2, id1}));
   ICING_ASSERT_OK(index->PersistToDisk());
   ICING_ASSERT_OK(index->Put(
       /*schema_type_id=*/3, /*joinable_property_id=*/10, /*document_id=*/6,
-      /*ref_namespace_fingerprint_ids=*/{id3}));
+      /*ref_namespace_id_uri_fingerprints=*/{id3}));
   ICING_ASSERT_OK(index->Put(
       /*schema_type_id=*/2, /*joinable_property_id=*/1, /*document_id=*/12,
-      /*ref_namespace_fingerprint_ids=*/{id4}));
+      /*ref_namespace_id_uri_fingerprints=*/{id4}));
   // GetChecksum should succeed without updating the checksum.
   ICING_EXPECT_OK(index->GetChecksum());
 
@@ -195,10 +195,10 @@
 
 TEST_F(QualifiedIdJoinIndexImplV2Test,
        InitializationShouldSucceedWithUpdateChecksums) {
-  NamespaceFingerprintIdentifier id1(/*namespace_id=*/1, /*fingerprint=*/12);
-  NamespaceFingerprintIdentifier id2(/*namespace_id=*/1, /*fingerprint=*/34);
-  NamespaceFingerprintIdentifier id3(/*namespace_id=*/1, /*fingerprint=*/56);
-  NamespaceFingerprintIdentifier id4(/*namespace_id=*/1, /*fingerprint=*/78);
+  NamespaceIdFingerprint id1(/*namespace_id=*/1, /*fingerprint=*/12);
+  NamespaceIdFingerprint id2(/*namespace_id=*/1, /*fingerprint=*/34);
+  NamespaceIdFingerprint id3(/*namespace_id=*/1, /*fingerprint=*/56);
+  NamespaceIdFingerprint id4(/*namespace_id=*/1, /*fingerprint=*/78);
 
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -209,13 +209,13 @@
   // Insert some data.
   ICING_ASSERT_OK(index1->Put(
       /*schema_type_id=*/2, /*joinable_property_id=*/1, /*document_id=*/5,
-      /*ref_namespace_fingerprint_ids=*/{id2, id1}));
+      /*ref_namespace_id_uri_fingerprints=*/{id2, id1}));
   ICING_ASSERT_OK(index1->Put(
       /*schema_type_id=*/3, /*joinable_property_id=*/10, /*document_id=*/6,
-      /*ref_namespace_fingerprint_ids=*/{id3}));
+      /*ref_namespace_id_uri_fingerprints=*/{id3}));
   ICING_ASSERT_OK(index1->Put(
       /*schema_type_id=*/2, /*joinable_property_id=*/1, /*document_id=*/12,
-      /*ref_namespace_fingerprint_ids=*/{id4}));
+      /*ref_namespace_id_uri_fingerprints=*/{id4}));
   ASSERT_THAT(index1, Pointee(SizeIs(4)));
 
   // After calling UpdateChecksums, all checksums should be recomputed and
@@ -232,26 +232,24 @@
   EXPECT_THAT(index2, Pointee(SizeIs(4)));
   EXPECT_THAT(
       GetJoinData(*index2, /*schema_type_id=*/2, /*joinable_property_id=*/1),
-      IsOkAndHolds(
-          ElementsAre(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                          /*document_id=*/12, /*join_info=*/id4),
-                      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                          /*document_id=*/5, /*join_info=*/id2),
-                      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                          /*document_id=*/5, /*join_info=*/id1))));
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/12, /*join_info=*/id4),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/5, /*join_info=*/id2),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/5, /*join_info=*/id1))));
   EXPECT_THAT(
       GetJoinData(*index2, /*schema_type_id=*/3, /*joinable_property_id=*/10),
-      IsOkAndHolds(
-          ElementsAre(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/6, /*join_info=*/id3))));
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/6, /*join_info=*/id3))));
 }
 
 TEST_F(QualifiedIdJoinIndexImplV2Test,
        InitializationShouldSucceedWithPersistToDisk) {
-  NamespaceFingerprintIdentifier id1(/*namespace_id=*/1, /*fingerprint=*/12);
-  NamespaceFingerprintIdentifier id2(/*namespace_id=*/1, /*fingerprint=*/34);
-  NamespaceFingerprintIdentifier id3(/*namespace_id=*/1, /*fingerprint=*/56);
-  NamespaceFingerprintIdentifier id4(/*namespace_id=*/1, /*fingerprint=*/78);
+  NamespaceIdFingerprint id1(/*namespace_id=*/1, /*fingerprint=*/12);
+  NamespaceIdFingerprint id2(/*namespace_id=*/1, /*fingerprint=*/34);
+  NamespaceIdFingerprint id3(/*namespace_id=*/1, /*fingerprint=*/56);
+  NamespaceIdFingerprint id4(/*namespace_id=*/1, /*fingerprint=*/78);
 
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -262,13 +260,13 @@
   // Insert some data.
   ICING_ASSERT_OK(index1->Put(
       /*schema_type_id=*/2, /*joinable_property_id=*/1, /*document_id=*/5,
-      /*ref_namespace_fingerprint_ids=*/{id2, id1}));
+      /*ref_namespace_id_uri_fingerprints=*/{id2, id1}));
   ICING_ASSERT_OK(index1->Put(
       /*schema_type_id=*/3, /*joinable_property_id=*/10, /*document_id=*/6,
-      /*ref_namespace_fingerprint_ids=*/{id3}));
+      /*ref_namespace_id_uri_fingerprints=*/{id3}));
   ICING_ASSERT_OK(index1->Put(
       /*schema_type_id=*/2, /*joinable_property_id=*/1, /*document_id=*/12,
-      /*ref_namespace_fingerprint_ids=*/{id4}));
+      /*ref_namespace_id_uri_fingerprints=*/{id4}));
   ASSERT_THAT(index1, Pointee(SizeIs(4)));
 
   // After calling PersistToDisk, all checksums should be recomputed and synced
@@ -283,26 +281,24 @@
   EXPECT_THAT(index2, Pointee(SizeIs(4)));
   EXPECT_THAT(
       GetJoinData(*index2, /*schema_type_id=*/2, /*joinable_property_id=*/1),
-      IsOkAndHolds(
-          ElementsAre(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                          /*document_id=*/12, /*join_info=*/id4),
-                      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                          /*document_id=*/5, /*join_info=*/id2),
-                      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                          /*document_id=*/5, /*join_info=*/id1))));
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/12, /*join_info=*/id4),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/5, /*join_info=*/id2),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/5, /*join_info=*/id1))));
   EXPECT_THAT(
       GetJoinData(*index2, /*schema_type_id=*/3, /*joinable_property_id=*/10),
-      IsOkAndHolds(
-          ElementsAre(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/6, /*join_info=*/id3))));
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/6, /*join_info=*/id3))));
 }
 
 TEST_F(QualifiedIdJoinIndexImplV2Test,
        InitializationShouldSucceedAfterDestruction) {
-  NamespaceFingerprintIdentifier id1(/*namespace_id=*/1, /*fingerprint=*/12);
-  NamespaceFingerprintIdentifier id2(/*namespace_id=*/1, /*fingerprint=*/34);
-  NamespaceFingerprintIdentifier id3(/*namespace_id=*/1, /*fingerprint=*/56);
-  NamespaceFingerprintIdentifier id4(/*namespace_id=*/1, /*fingerprint=*/78);
+  NamespaceIdFingerprint id1(/*namespace_id=*/1, /*fingerprint=*/12);
+  NamespaceIdFingerprint id2(/*namespace_id=*/1, /*fingerprint=*/34);
+  NamespaceIdFingerprint id3(/*namespace_id=*/1, /*fingerprint=*/56);
+  NamespaceIdFingerprint id4(/*namespace_id=*/1, /*fingerprint=*/78);
 
   {
     // Create new qualified id join index
@@ -314,13 +310,13 @@
     // Insert some data.
     ICING_ASSERT_OK(index->Put(
         /*schema_type_id=*/2, /*joinable_property_id=*/1, /*document_id=*/5,
-        /*ref_namespace_fingerprint_ids=*/{id2, id1}));
+        /*ref_namespace_id_uri_fingerprints=*/{id2, id1}));
     ICING_ASSERT_OK(index->Put(
         /*schema_type_id=*/3, /*joinable_property_id=*/10, /*document_id=*/6,
-        /*ref_namespace_fingerprint_ids=*/{id3}));
+        /*ref_namespace_id_uri_fingerprints=*/{id3}));
     ICING_ASSERT_OK(index->Put(
         /*schema_type_id=*/2, /*joinable_property_id=*/1, /*document_id=*/12,
-        /*ref_namespace_fingerprint_ids=*/{id4}));
+        /*ref_namespace_id_uri_fingerprints=*/{id4}));
     ASSERT_THAT(index, Pointee(SizeIs(4)));
   }
 
@@ -336,18 +332,16 @@
     EXPECT_THAT(index, Pointee(SizeIs(4)));
     EXPECT_THAT(
         GetJoinData(*index, /*schema_type_id=*/2, /*joinable_property_id=*/1),
-        IsOkAndHolds(
-            ElementsAre(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                            /*document_id=*/12, /*join_info=*/id4),
-                        DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                            /*document_id=*/5, /*join_info=*/id2),
-                        DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                            /*document_id=*/5, /*join_info=*/id1))));
+        IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                     /*document_id=*/12, /*join_info=*/id4),
+                                 DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                     /*document_id=*/5, /*join_info=*/id2),
+                                 DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                     /*document_id=*/5, /*join_info=*/id1))));
     EXPECT_THAT(
         GetJoinData(*index, /*schema_type_id=*/3, /*joinable_property_id=*/10),
-        IsOkAndHolds(
-            ElementsAre(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                /*document_id=*/6, /*join_info=*/id3))));
+        IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+            /*document_id=*/6, /*join_info=*/id3))));
   }
 }
 
@@ -361,9 +355,8 @@
                                            /*pre_mapping_fbv=*/false));
     ICING_ASSERT_OK(index->Put(
         /*schema_type_id=*/2, /*joinable_property_id=*/1, /*document_id=*/5,
-        /*ref_namespace_fingerprint_ids=*/
-        {NamespaceFingerprintIdentifier(/*namespace_id=*/1,
-                                        /*fingerprint=*/12)}));
+        /*ref_namespace_id_uri_fingerprints=*/
+        {NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/12)}));
 
     ICING_ASSERT_OK(index->PersistToDisk());
   }
@@ -414,10 +407,9 @@
         QualifiedIdJoinIndexImplV2::Create(filesystem_, working_path_,
                                            /*pre_mapping_fbv=*/false));
     ICING_ASSERT_OK(index->Put(
-        /*schema_type_id=*/2, /*joinable_property_id=*/1, /*document_id=*/5,
-        /*ref_namespace_fingerprint_ids=*/
-        {NamespaceFingerprintIdentifier(/*namespace_id=*/1,
-                                        /*fingerprint=*/12)}));
+        /*schema_type_id=*/2, /*joinable_property_id=*/1,
+        /*document_id=*/5, /*ref_namespace_id_uri_fingerprints=*/
+        {NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/12)}));
 
     ICING_ASSERT_OK(index->PersistToDisk());
   }
@@ -464,10 +456,9 @@
         QualifiedIdJoinIndexImplV2::Create(filesystem_, working_path_,
                                            /*pre_mapping_fbv=*/false));
     ICING_ASSERT_OK(index->Put(
-        /*schema_type_id=*/2, /*joinable_property_id=*/1, /*document_id=*/5,
-        /*ref_namespace_fingerprint_ids=*/
-        {NamespaceFingerprintIdentifier(/*namespace_id=*/1,
-                                        /*fingerprint=*/12)}));
+        /*schema_type_id=*/2, /*joinable_property_id=*/1,
+        /*document_id=*/5, /*ref_namespace_id_uri_fingerprints=*/
+        {NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/12)}));
 
     ICING_ASSERT_OK(index->PersistToDisk());
   }
@@ -516,10 +507,9 @@
         QualifiedIdJoinIndexImplV2::Create(filesystem_, working_path_,
                                            /*pre_mapping_fbv=*/false));
     ICING_ASSERT_OK(index->Put(
-        /*schema_type_id=*/2, /*joinable_property_id=*/1, /*document_id=*/5,
-        /*ref_namespace_fingerprint_ids=*/
-        {NamespaceFingerprintIdentifier(/*namespace_id=*/1,
-                                        /*fingerprint=*/12)}));
+        /*schema_type_id=*/2, /*joinable_property_id=*/1,
+        /*document_id=*/5, /*ref_namespace_id_uri_fingerprints=*/
+        {NamespaceIdFingerprint(/*namespace_id=*/1, /*fingerprint=*/12)}));
 
     ICING_ASSERT_OK(index->PersistToDisk());
   }
@@ -549,7 +539,7 @@
 }
 
 TEST_F(QualifiedIdJoinIndexImplV2Test, InvalidPut) {
-  NamespaceFingerprintIdentifier id(/*namespace_id=*/1, /*fingerprint=*/12);
+  NamespaceIdFingerprint id(/*namespace_id=*/1, /*fingerprint=*/12);
 
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -559,15 +549,15 @@
 
   EXPECT_THAT(
       index->Put(/*schema_type_id=*/-1, /*joinable_property_id=*/1,
-                 /*document_id=*/5, /*ref_namespace_fingerprint_ids=*/{id}),
+                 /*document_id=*/5, /*ref_namespace_id_uri_fingerprints=*/{id}),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(
       index->Put(/*schema_type_id=*/2, /*joinable_property_id=*/-1,
-                 /*document_id=*/5, /*ref_namespace_fingerprint_ids=*/{id}),
+                 /*document_id=*/5, /*ref_namespace_id_uri_fingerprints=*/{id}),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(index->Put(/*schema_type_id=*/2, /*joinable_property_id=*/1,
                          /*document_id=*/kInvalidDocumentId,
-                         /*ref_namespace_fingerprint_ids=*/{id}),
+                         /*ref_namespace_id_uri_fingerprints=*/{id}),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -599,7 +589,7 @@
 
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/5,
-                 /*ref_namespace_fingerprint_ids=*/{}),
+                 /*ref_namespace_id_uri_fingerprints=*/{}),
       IsOk());
   EXPECT_THAT(index, Pointee(IsEmpty()));
 
@@ -616,10 +606,10 @@
   SchemaTypeId schema_type_id = 2;
   JoinablePropertyId joinable_property_id = 1;
 
-  NamespaceFingerprintIdentifier id1(/*namespace_id=*/3, /*fingerprint=*/12);
-  NamespaceFingerprintIdentifier id2(/*namespace_id=*/1, /*fingerprint=*/34);
-  NamespaceFingerprintIdentifier id3(/*namespace_id=*/2, /*fingerprint=*/56);
-  NamespaceFingerprintIdentifier id4(/*namespace_id=*/0, /*fingerprint=*/78);
+  NamespaceIdFingerprint id1(/*namespace_id=*/3, /*fingerprint=*/12);
+  NamespaceIdFingerprint id2(/*namespace_id=*/1, /*fingerprint=*/34);
+  NamespaceIdFingerprint id3(/*namespace_id=*/2, /*fingerprint=*/56);
+  NamespaceIdFingerprint id4(/*namespace_id=*/0, /*fingerprint=*/78);
 
   {
     // Create new qualified id join index
@@ -630,28 +620,28 @@
 
     EXPECT_THAT(
         index->Put(schema_type_id, joinable_property_id, /*document_id=*/5,
-                   /*ref_namespace_fingerprint_ids=*/{id2, id1}),
+                   /*ref_namespace_id_uri_fingerprints=*/{id2, id1}),
         IsOk());
     EXPECT_THAT(
         index->Put(schema_type_id, joinable_property_id, /*document_id=*/6,
-                   /*ref_namespace_fingerprint_ids=*/{id3}),
+                   /*ref_namespace_id_uri_fingerprints=*/{id3}),
         IsOk());
     EXPECT_THAT(
         index->Put(schema_type_id, joinable_property_id, /*document_id=*/12,
-                   /*ref_namespace_fingerprint_ids=*/{id4}),
+                   /*ref_namespace_id_uri_fingerprints=*/{id4}),
         IsOk());
     EXPECT_THAT(index, Pointee(SizeIs(4)));
 
-    EXPECT_THAT(GetJoinData(*index, schema_type_id, joinable_property_id),
-                IsOkAndHolds(ElementsAre(
-                    DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                        /*document_id=*/12, /*join_info=*/id4),
-                    DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                        /*document_id=*/6, /*join_info=*/id3),
-                    DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                        /*document_id=*/5, /*join_info=*/id1),
-                    DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                        /*document_id=*/5, /*join_info=*/id2))));
+    EXPECT_THAT(
+        GetJoinData(*index, schema_type_id, joinable_property_id),
+        IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                     /*document_id=*/12, /*join_info=*/id4),
+                                 DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                     /*document_id=*/6, /*join_info=*/id3),
+                                 DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                     /*document_id=*/5, /*join_info=*/id1),
+                                 DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                     /*document_id=*/5, /*join_info=*/id2))));
     EXPECT_THAT(GetJoinData(*index, schema_type_id + 1, joinable_property_id),
                 IsOkAndHolds(IsEmpty()));
     EXPECT_THAT(GetJoinData(*index, schema_type_id, joinable_property_id + 1),
@@ -666,16 +656,16 @@
       QualifiedIdJoinIndexImplV2::Create(filesystem_, working_path_,
                                          /*pre_mapping_fbv=*/false));
   EXPECT_THAT(index, Pointee(SizeIs(4)));
-  EXPECT_THAT(GetJoinData(*index, schema_type_id, joinable_property_id),
-              IsOkAndHolds(ElementsAre(
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/12, /*join_info=*/id4),
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/6, /*join_info=*/id3),
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/5, /*join_info=*/id1),
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/5, /*join_info=*/id2))));
+  EXPECT_THAT(
+      GetJoinData(*index, schema_type_id, joinable_property_id),
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/12, /*join_info=*/id4),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/6, /*join_info=*/id3),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/5, /*join_info=*/id1),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/5, /*join_info=*/id2))));
   EXPECT_THAT(GetJoinData(*index, schema_type_id + 1, joinable_property_id),
               IsOkAndHolds(IsEmpty()));
   EXPECT_THAT(GetJoinData(*index, schema_type_id, joinable_property_id + 1),
@@ -690,10 +680,10 @@
   JoinablePropertyId joinable_property_id1 = 1;
   JoinablePropertyId joinable_property_id2 = 10;
 
-  NamespaceFingerprintIdentifier id1(/*namespace_id=*/3, /*fingerprint=*/12);
-  NamespaceFingerprintIdentifier id2(/*namespace_id=*/1, /*fingerprint=*/34);
-  NamespaceFingerprintIdentifier id3(/*namespace_id=*/2, /*fingerprint=*/56);
-  NamespaceFingerprintIdentifier id4(/*namespace_id=*/0, /*fingerprint=*/78);
+  NamespaceIdFingerprint id1(/*namespace_id=*/3, /*fingerprint=*/12);
+  NamespaceIdFingerprint id2(/*namespace_id=*/1, /*fingerprint=*/34);
+  NamespaceIdFingerprint id3(/*namespace_id=*/2, /*fingerprint=*/56);
+  NamespaceIdFingerprint id4(/*namespace_id=*/0, /*fingerprint=*/78);
 
   {
     // Create new qualified id join index
@@ -704,38 +694,38 @@
 
     EXPECT_THAT(
         index->Put(schema_type_id1, joinable_property_id1, /*document_id=*/5,
-                   /*ref_namespace_fingerprint_ids=*/{id1}),
+                   /*ref_namespace_id_uri_fingerprints=*/{id1}),
         IsOk());
     EXPECT_THAT(
         index->Put(schema_type_id1, joinable_property_id2, /*document_id=*/5,
-                   /*ref_namespace_fingerprint_ids=*/{id2}),
+                   /*ref_namespace_id_uri_fingerprints=*/{id2}),
         IsOk());
     EXPECT_THAT(
         index->Put(schema_type_id2, joinable_property_id1, /*document_id=*/12,
-                   /*ref_namespace_fingerprint_ids=*/{id3}),
+                   /*ref_namespace_id_uri_fingerprints=*/{id3}),
         IsOk());
     EXPECT_THAT(
         index->Put(schema_type_id2, joinable_property_id2, /*document_id=*/12,
-                   /*ref_namespace_fingerprint_ids=*/{id4}),
+                   /*ref_namespace_id_uri_fingerprints=*/{id4}),
         IsOk());
     EXPECT_THAT(index, Pointee(SizeIs(4)));
 
-    EXPECT_THAT(GetJoinData(*index, schema_type_id1, joinable_property_id1),
-                IsOkAndHolds(ElementsAre(
-                    DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                        /*document_id=*/5, /*join_info=*/id1))));
-    EXPECT_THAT(GetJoinData(*index, schema_type_id1, joinable_property_id2),
-                IsOkAndHolds(ElementsAre(
-                    DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                        /*document_id=*/5, /*join_info=*/id2))));
-    EXPECT_THAT(GetJoinData(*index, schema_type_id2, joinable_property_id1),
-                IsOkAndHolds(ElementsAre(
-                    DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                        /*document_id=*/12, /*join_info=*/id3))));
-    EXPECT_THAT(GetJoinData(*index, schema_type_id2, joinable_property_id2),
-                IsOkAndHolds(ElementsAre(
-                    DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                        /*document_id=*/12, /*join_info=*/id4))));
+    EXPECT_THAT(
+        GetJoinData(*index, schema_type_id1, joinable_property_id1),
+        IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+            /*document_id=*/5, /*join_info=*/id1))));
+    EXPECT_THAT(
+        GetJoinData(*index, schema_type_id1, joinable_property_id2),
+        IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+            /*document_id=*/5, /*join_info=*/id2))));
+    EXPECT_THAT(
+        GetJoinData(*index, schema_type_id2, joinable_property_id1),
+        IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+            /*document_id=*/12, /*join_info=*/id3))));
+    EXPECT_THAT(
+        GetJoinData(*index, schema_type_id2, joinable_property_id2),
+        IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+            /*document_id=*/12, /*join_info=*/id4))));
 
     ICING_ASSERT_OK(index->PersistToDisk());
   }
@@ -746,22 +736,22 @@
       QualifiedIdJoinIndexImplV2::Create(filesystem_, working_path_,
                                          /*pre_mapping_fbv=*/false));
   EXPECT_THAT(index, Pointee(SizeIs(4)));
-  EXPECT_THAT(GetJoinData(*index, schema_type_id1, joinable_property_id1),
-              IsOkAndHolds(ElementsAre(
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/5, /*join_info=*/id1))));
-  EXPECT_THAT(GetJoinData(*index, schema_type_id1, joinable_property_id2),
-              IsOkAndHolds(ElementsAre(
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/5, /*join_info=*/id2))));
-  EXPECT_THAT(GetJoinData(*index, schema_type_id2, joinable_property_id1),
-              IsOkAndHolds(ElementsAre(
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/12, /*join_info=*/id3))));
-  EXPECT_THAT(GetJoinData(*index, schema_type_id2, joinable_property_id2),
-              IsOkAndHolds(ElementsAre(
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/12, /*join_info=*/id4))));
+  EXPECT_THAT(
+      GetJoinData(*index, schema_type_id1, joinable_property_id1),
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/5, /*join_info=*/id1))));
+  EXPECT_THAT(
+      GetJoinData(*index, schema_type_id1, joinable_property_id2),
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/5, /*join_info=*/id2))));
+  EXPECT_THAT(
+      GetJoinData(*index, schema_type_id2, joinable_property_id1),
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/12, /*join_info=*/id3))));
+  EXPECT_THAT(
+      GetJoinData(*index, schema_type_id2, joinable_property_id2),
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/12, /*join_info=*/id4))));
 }
 
 TEST_F(QualifiedIdJoinIndexImplV2Test, SetLastAddedDocumentId) {
@@ -813,34 +803,34 @@
   JoinablePropertyId joinable_property_id1 = 11;
   JoinablePropertyId joinable_property_id2 = 15;
 
-  NamespaceFingerprintIdentifier id1(/*namespace_id=*/2, /*fingerprint=*/101);
-  NamespaceFingerprintIdentifier id2(/*namespace_id=*/3, /*fingerprint=*/102);
-  NamespaceFingerprintIdentifier id3(/*namespace_id=*/4, /*fingerprint=*/103);
-  NamespaceFingerprintIdentifier id4(/*namespace_id=*/0, /*fingerprint=*/104);
-  NamespaceFingerprintIdentifier id5(/*namespace_id=*/0, /*fingerprint=*/105);
-  NamespaceFingerprintIdentifier id6(/*namespace_id=*/1, /*fingerprint=*/106);
-  NamespaceFingerprintIdentifier id7(/*namespace_id=*/3, /*fingerprint=*/107);
-  NamespaceFingerprintIdentifier id8(/*namespace_id=*/2, /*fingerprint=*/108);
+  NamespaceIdFingerprint id1(/*namespace_id=*/2, /*fingerprint=*/101);
+  NamespaceIdFingerprint id2(/*namespace_id=*/3, /*fingerprint=*/102);
+  NamespaceIdFingerprint id3(/*namespace_id=*/4, /*fingerprint=*/103);
+  NamespaceIdFingerprint id4(/*namespace_id=*/0, /*fingerprint=*/104);
+  NamespaceIdFingerprint id5(/*namespace_id=*/0, /*fingerprint=*/105);
+  NamespaceIdFingerprint id6(/*namespace_id=*/1, /*fingerprint=*/106);
+  NamespaceIdFingerprint id7(/*namespace_id=*/3, /*fingerprint=*/107);
+  NamespaceIdFingerprint id8(/*namespace_id=*/2, /*fingerprint=*/108);
 
   EXPECT_THAT(
       index->Put(schema_type_id1, joinable_property_id1, /*document_id=*/3,
-                 /*ref_namespace_fingerprint_ids=*/{id1, id2, id3}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id1, id2, id3}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id2, joinable_property_id2, /*document_id=*/5,
-                 /*ref_namespace_fingerprint_ids=*/{id4}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id4}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id2, joinable_property_id2, /*document_id=*/8,
-                 /*ref_namespace_fingerprint_ids=*/{id5, id6}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id5, id6}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id1, joinable_property_id1, /*document_id=*/13,
-                 /*ref_namespace_fingerprint_ids=*/{id7}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id7}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id1, joinable_property_id1, /*document_id=*/21,
-                 /*ref_namespace_fingerprint_ids=*/{id8}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id8}),
       IsOk());
   index->set_last_added_document_id(21);
 
@@ -877,15 +867,14 @@
   //   - old_doc_id=3, old_ref_namespace_id=2: NOT FOUND
   //
   // For new_doc_id=0, it should reorder due to posting list restriction.
-  EXPECT_THAT(
-      GetJoinData(*index, schema_type_id1, joinable_property_id1),
-      IsOkAndHolds(ElementsAre(
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/0, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/2, /*fingerprint=*/102)),
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/0, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/0, /*fingerprint=*/103)))));
+  EXPECT_THAT(GetJoinData(*index, schema_type_id1, joinable_property_id1),
+              IsOkAndHolds(ElementsAre(
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/0, /*join_info=*/NamespaceIdFingerprint(
+                          /*namespace_id=*/2, /*fingerprint=*/102)),
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/0, /*join_info=*/NamespaceIdFingerprint(
+                          /*namespace_id=*/0, /*fingerprint=*/103)))));
 
   // 2) schema_type_id2, joinable_property_id2:
   //   - old_doc_id=8, old_ref_namespace_id=1: NOT FOUND
@@ -894,32 +883,30 @@
   //   - old_doc_id=5, old_ref_namespace_id=0: NOT FOUND
   EXPECT_THAT(
       GetJoinData(*index, schema_type_id2, joinable_property_id2),
-      IsOkAndHolds(
-          ElementsAre(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/1, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/1, /*fingerprint=*/105)))));
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/1, /*join_info=*/NamespaceIdFingerprint(
+              /*namespace_id=*/1, /*fingerprint=*/105)))));
 
   // Verify Put API should work normally after Optimize().
-  NamespaceFingerprintIdentifier id9(/*namespace_id=*/1, /*fingerprint=*/109);
+  NamespaceIdFingerprint id9(/*namespace_id=*/1, /*fingerprint=*/109);
   EXPECT_THAT(
       index->Put(schema_type_id1, joinable_property_id1, /*document_id=*/99,
-                 /*ref_namespace_fingerprint_ids=*/{id9}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id9}),
       IsOk());
   index->set_last_added_document_id(99);
 
   EXPECT_THAT(index, Pointee(SizeIs(4)));
   EXPECT_THAT(index->last_added_document_id(), Eq(99));
-  EXPECT_THAT(
-      GetJoinData(*index, schema_type_id1, joinable_property_id1),
-      IsOkAndHolds(ElementsAre(
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/99, /*join_info=*/id9),
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/0, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/2, /*fingerprint=*/102)),
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/0, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/0, /*fingerprint=*/103)))));
+  EXPECT_THAT(GetJoinData(*index, schema_type_id1, joinable_property_id1),
+              IsOkAndHolds(ElementsAre(
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/99, /*join_info=*/id9),
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/0, /*join_info=*/NamespaceIdFingerprint(
+                          /*namespace_id=*/2, /*fingerprint=*/102)),
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/0, /*join_info=*/NamespaceIdFingerprint(
+                          /*namespace_id=*/0, /*fingerprint=*/103)))));
 }
 
 TEST_F(QualifiedIdJoinIndexImplV2Test, OptimizeDocumentIdChange) {
@@ -933,32 +920,32 @@
   SchemaTypeId schema_type_id = 2;
   JoinablePropertyId joinable_property_id = 1;
 
-  NamespaceFingerprintIdentifier id1(/*namespace_id=*/1, /*fingerprint=*/101);
-  NamespaceFingerprintIdentifier id2(/*namespace_id=*/1, /*fingerprint=*/102);
-  NamespaceFingerprintIdentifier id3(/*namespace_id=*/1, /*fingerprint=*/103);
-  NamespaceFingerprintIdentifier id4(/*namespace_id=*/1, /*fingerprint=*/104);
-  NamespaceFingerprintIdentifier id5(/*namespace_id=*/1, /*fingerprint=*/105);
-  NamespaceFingerprintIdentifier id6(/*namespace_id=*/1, /*fingerprint=*/106);
+  NamespaceIdFingerprint id1(/*namespace_id=*/1, /*fingerprint=*/101);
+  NamespaceIdFingerprint id2(/*namespace_id=*/1, /*fingerprint=*/102);
+  NamespaceIdFingerprint id3(/*namespace_id=*/1, /*fingerprint=*/103);
+  NamespaceIdFingerprint id4(/*namespace_id=*/1, /*fingerprint=*/104);
+  NamespaceIdFingerprint id5(/*namespace_id=*/1, /*fingerprint=*/105);
+  NamespaceIdFingerprint id6(/*namespace_id=*/1, /*fingerprint=*/106);
 
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/3,
-                 /*ref_namespace_fingerprint_ids=*/{id1, id2}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id1, id2}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/5,
-                 /*ref_namespace_fingerprint_ids=*/{id3}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id3}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/8,
-                 /*ref_namespace_fingerprint_ids=*/{id4}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id4}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/13,
-                 /*ref_namespace_fingerprint_ids=*/{id5}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id5}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/21,
-                 /*ref_namespace_fingerprint_ids=*/{id6}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id6}),
       IsOk());
   index->set_last_added_document_id(21);
 
@@ -987,39 +974,39 @@
   // - old_doc_id=5, join_info=id3: NOT FOUND
   // - old_doc_id=3, join_info=id2: become doc_id=0, join_info=id2
   // - old_doc_id=3, join_info=id1: become doc_id=0, join_info=id1
-  EXPECT_THAT(GetJoinData(*index, schema_type_id, joinable_property_id),
-              IsOkAndHolds(ElementsAre(
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/2, /*join_info=*/id6),
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/1, /*join_info=*/id5),
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/0, /*join_info=*/id2),
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/0, /*join_info=*/id1))));
+  EXPECT_THAT(
+      GetJoinData(*index, schema_type_id, joinable_property_id),
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/2, /*join_info=*/id6),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/1, /*join_info=*/id5),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/0, /*join_info=*/id2),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/0, /*join_info=*/id1))));
 
   // Verify Put API should work normally after Optimize().
-  NamespaceFingerprintIdentifier id7(/*namespace_id=*/1, /*fingerprint=*/107);
+  NamespaceIdFingerprint id7(/*namespace_id=*/1, /*fingerprint=*/107);
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/99,
-                 /*ref_namespace_fingerprint_ids=*/{id7}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id7}),
       IsOk());
   index->set_last_added_document_id(99);
 
   EXPECT_THAT(index, Pointee(SizeIs(5)));
   EXPECT_THAT(index->last_added_document_id(), Eq(99));
-  EXPECT_THAT(GetJoinData(*index, schema_type_id, joinable_property_id),
-              IsOkAndHolds(ElementsAre(
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/99, /*join_info=*/id7),
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/2, /*join_info=*/id6),
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/1, /*join_info=*/id5),
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/0, /*join_info=*/id2),
-                  DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                      /*document_id=*/0, /*join_info=*/id1))));
+  EXPECT_THAT(
+      GetJoinData(*index, schema_type_id, joinable_property_id),
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/99, /*join_info=*/id7),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/2, /*join_info=*/id6),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/1, /*join_info=*/id5),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/0, /*join_info=*/id2),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/0, /*join_info=*/id1))));
 }
 
 TEST_F(QualifiedIdJoinIndexImplV2Test, OptimizeOutOfRangeDocumentId) {
@@ -1032,11 +1019,11 @@
 
   SchemaTypeId schema_type_id = 2;
   JoinablePropertyId joinable_property_id = 1;
-  NamespaceFingerprintIdentifier id(/*namespace_id=*/1, /*fingerprint=*/101);
+  NamespaceIdFingerprint id(/*namespace_id=*/1, /*fingerprint=*/101);
 
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/99,
-                 /*ref_namespace_fingerprint_ids=*/{id}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id}),
       IsOk());
   index->set_last_added_document_id(99);
 
@@ -1069,32 +1056,32 @@
   SchemaTypeId schema_type_id = 2;
   JoinablePropertyId joinable_property_id = 1;
 
-  NamespaceFingerprintIdentifier id1(/*namespace_id=*/1, /*fingerprint=*/101);
-  NamespaceFingerprintIdentifier id2(/*namespace_id=*/1, /*fingerprint=*/102);
-  NamespaceFingerprintIdentifier id3(/*namespace_id=*/1, /*fingerprint=*/103);
-  NamespaceFingerprintIdentifier id4(/*namespace_id=*/1, /*fingerprint=*/104);
-  NamespaceFingerprintIdentifier id5(/*namespace_id=*/1, /*fingerprint=*/105);
-  NamespaceFingerprintIdentifier id6(/*namespace_id=*/1, /*fingerprint=*/106);
+  NamespaceIdFingerprint id1(/*namespace_id=*/1, /*fingerprint=*/101);
+  NamespaceIdFingerprint id2(/*namespace_id=*/1, /*fingerprint=*/102);
+  NamespaceIdFingerprint id3(/*namespace_id=*/1, /*fingerprint=*/103);
+  NamespaceIdFingerprint id4(/*namespace_id=*/1, /*fingerprint=*/104);
+  NamespaceIdFingerprint id5(/*namespace_id=*/1, /*fingerprint=*/105);
+  NamespaceIdFingerprint id6(/*namespace_id=*/1, /*fingerprint=*/106);
 
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/3,
-                 /*ref_namespace_fingerprint_ids=*/{id1, id2}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id1, id2}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/5,
-                 /*ref_namespace_fingerprint_ids=*/{id3}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id3}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/8,
-                 /*ref_namespace_fingerprint_ids=*/{id4}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id4}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/13,
-                 /*ref_namespace_fingerprint_ids=*/{id5}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id5}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/21,
-                 /*ref_namespace_fingerprint_ids=*/{id6}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id6}),
       IsOk());
   index->set_last_added_document_id(21);
 
@@ -1129,36 +1116,36 @@
   SchemaTypeId schema_type_id = 2;
   JoinablePropertyId joinable_property_id = 1;
 
-  NamespaceFingerprintIdentifier id1(/*namespace_id=*/3, /*fingerprint=*/101);
-  NamespaceFingerprintIdentifier id2(/*namespace_id=*/5, /*fingerprint=*/102);
-  NamespaceFingerprintIdentifier id3(/*namespace_id=*/4, /*fingerprint=*/103);
-  NamespaceFingerprintIdentifier id4(/*namespace_id=*/0, /*fingerprint=*/104);
-  NamespaceFingerprintIdentifier id5(/*namespace_id=*/2, /*fingerprint=*/105);
-  NamespaceFingerprintIdentifier id6(/*namespace_id=*/1, /*fingerprint=*/106);
+  NamespaceIdFingerprint id1(/*namespace_id=*/3, /*fingerprint=*/101);
+  NamespaceIdFingerprint id2(/*namespace_id=*/5, /*fingerprint=*/102);
+  NamespaceIdFingerprint id3(/*namespace_id=*/4, /*fingerprint=*/103);
+  NamespaceIdFingerprint id4(/*namespace_id=*/0, /*fingerprint=*/104);
+  NamespaceIdFingerprint id5(/*namespace_id=*/2, /*fingerprint=*/105);
+  NamespaceIdFingerprint id6(/*namespace_id=*/1, /*fingerprint=*/106);
 
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/2,
-                 /*ref_namespace_fingerprint_ids=*/{id1}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id1}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/3,
-                 /*ref_namespace_fingerprint_ids=*/{id2}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id2}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/5,
-                 /*ref_namespace_fingerprint_ids=*/{id3}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id3}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/8,
-                 /*ref_namespace_fingerprint_ids=*/{id4}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id4}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/13,
-                 /*ref_namespace_fingerprint_ids=*/{id5}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id5}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/21,
-                 /*ref_namespace_fingerprint_ids=*/{id6}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id6}),
       IsOk());
   index->set_last_added_document_id(21);
 
@@ -1190,49 +1177,47 @@
   // - id3 (old_namespace_id=4): NOT FOUND
   // - id2 (old_namespace_id=5): new_namespace_id=0 (document_id = 3)
   // - id1 (old_namespace_id=3): new_namespace_id=1 (document_id = 2)
-  EXPECT_THAT(
-      GetJoinData(*index, schema_type_id, joinable_property_id),
-      IsOkAndHolds(ElementsAre(
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/21, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/3, /*fingerprint=*/106)),
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/8, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/2, /*fingerprint=*/104)),
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/3, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/0, /*fingerprint=*/102)),
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/2, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/1, /*fingerprint=*/101)))));
+  EXPECT_THAT(GetJoinData(*index, schema_type_id, joinable_property_id),
+              IsOkAndHolds(ElementsAre(
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/21, /*join_info=*/NamespaceIdFingerprint(
+                          /*namespace_id=*/3, /*fingerprint=*/106)),
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/8, /*join_info=*/NamespaceIdFingerprint(
+                          /*namespace_id=*/2, /*fingerprint=*/104)),
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/3, /*join_info=*/NamespaceIdFingerprint(
+                          /*namespace_id=*/0, /*fingerprint=*/102)),
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/2, /*join_info=*/NamespaceIdFingerprint(
+                          /*namespace_id=*/1, /*fingerprint=*/101)))));
 
   // Verify Put API should work normally after Optimize().
-  NamespaceFingerprintIdentifier id7(/*namespace_id=*/1, /*fingerprint=*/107);
+  NamespaceIdFingerprint id7(/*namespace_id=*/1, /*fingerprint=*/107);
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/99,
-                 /*ref_namespace_fingerprint_ids=*/{id7}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id7}),
       IsOk());
   index->set_last_added_document_id(99);
 
   EXPECT_THAT(index, Pointee(SizeIs(5)));
   EXPECT_THAT(index->last_added_document_id(), Eq(99));
-  EXPECT_THAT(
-      GetJoinData(*index, schema_type_id, joinable_property_id),
-      IsOkAndHolds(ElementsAre(
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/99, /*join_info=*/id7),
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/21, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/3, /*fingerprint=*/106)),
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/8, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/2, /*fingerprint=*/104)),
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/3, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/0, /*fingerprint=*/102)),
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/2, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/1, /*fingerprint=*/101)))));
+  EXPECT_THAT(GetJoinData(*index, schema_type_id, joinable_property_id),
+              IsOkAndHolds(ElementsAre(
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/99, /*join_info=*/id7),
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/21, /*join_info=*/NamespaceIdFingerprint(
+                          /*namespace_id=*/3, /*fingerprint=*/106)),
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/8, /*join_info=*/NamespaceIdFingerprint(
+                          /*namespace_id=*/2, /*fingerprint=*/104)),
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/3, /*join_info=*/NamespaceIdFingerprint(
+                          /*namespace_id=*/0, /*fingerprint=*/102)),
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/2, /*join_info=*/NamespaceIdFingerprint(
+                          /*namespace_id=*/1, /*fingerprint=*/101)))));
 }
 
 TEST_F(QualifiedIdJoinIndexImplV2Test, OptimizeNamespaceIdChangeShouldReorder) {
@@ -1246,18 +1231,18 @@
   SchemaTypeId schema_type_id = 2;
   JoinablePropertyId joinable_property_id = 1;
 
-  NamespaceFingerprintIdentifier id1(/*namespace_id=*/0, /*fingerprint=*/101);
-  NamespaceFingerprintIdentifier id2(/*namespace_id=*/1, /*fingerprint=*/102);
-  NamespaceFingerprintIdentifier id3(/*namespace_id=*/2, /*fingerprint=*/103);
-  NamespaceFingerprintIdentifier id4(/*namespace_id=*/1, /*fingerprint=*/104);
+  NamespaceIdFingerprint id1(/*namespace_id=*/0, /*fingerprint=*/101);
+  NamespaceIdFingerprint id2(/*namespace_id=*/1, /*fingerprint=*/102);
+  NamespaceIdFingerprint id3(/*namespace_id=*/2, /*fingerprint=*/103);
+  NamespaceIdFingerprint id4(/*namespace_id=*/1, /*fingerprint=*/104);
 
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/0,
-                 /*ref_namespace_fingerprint_ids=*/{id1, id2, id3}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id1, id2, id3}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/1,
-                 /*ref_namespace_fingerprint_ids=*/{id4}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id4}),
       IsOk());
   index->set_last_added_document_id(1);
 
@@ -1283,21 +1268,20 @@
   // - id1 (old_namespace_id=0): new_namespace_id=2 (document_id = 0)
   //
   // Should reorder to [id4, id1, id3, id2] due to posting list restriction.
-  EXPECT_THAT(
-      GetJoinData(*index, schema_type_id, joinable_property_id),
-      IsOkAndHolds(ElementsAre(
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/1, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/0, /*fingerprint=*/104)),
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/0, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/2, /*fingerprint=*/101)),
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/0, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/1, /*fingerprint=*/103)),
-          DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/0, /*join_info=*/NamespaceFingerprintIdentifier(
-                  /*namespace_id=*/0, /*fingerprint=*/102)))));
+  EXPECT_THAT(GetJoinData(*index, schema_type_id, joinable_property_id),
+              IsOkAndHolds(ElementsAre(
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/1, /*join_info=*/NamespaceIdFingerprint(
+                          /*namespace_id=*/0, /*fingerprint=*/104)),
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/0, /*join_info=*/NamespaceIdFingerprint(
+                          /*namespace_id=*/2, /*fingerprint=*/101)),
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/0, /*join_info=*/NamespaceIdFingerprint(
+                          /*namespace_id=*/1, /*fingerprint=*/103)),
+                  DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                      /*document_id=*/0, /*join_info=*/NamespaceIdFingerprint(
+                          /*namespace_id=*/0, /*fingerprint=*/102)))));
 }
 
 TEST_F(QualifiedIdJoinIndexImplV2Test, OptimizeOutOfRangeNamespaceId) {
@@ -1310,11 +1294,11 @@
 
   SchemaTypeId schema_type_id = 2;
   JoinablePropertyId joinable_property_id = 1;
-  NamespaceFingerprintIdentifier id(/*namespace_id=*/99, /*fingerprint=*/101);
+  NamespaceIdFingerprint id(/*namespace_id=*/99, /*fingerprint=*/101);
 
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/0,
-                 /*ref_namespace_fingerprint_ids=*/{id}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id}),
       IsOk());
   index->set_last_added_document_id(0);
 
@@ -1347,21 +1331,21 @@
   SchemaTypeId schema_type_id = 2;
   JoinablePropertyId joinable_property_id = 1;
 
-  NamespaceFingerprintIdentifier id1(/*namespace_id=*/0, /*fingerprint=*/101);
-  NamespaceFingerprintIdentifier id2(/*namespace_id=*/1, /*fingerprint=*/102);
-  NamespaceFingerprintIdentifier id3(/*namespace_id=*/2, /*fingerprint=*/103);
+  NamespaceIdFingerprint id1(/*namespace_id=*/0, /*fingerprint=*/101);
+  NamespaceIdFingerprint id2(/*namespace_id=*/1, /*fingerprint=*/102);
+  NamespaceIdFingerprint id3(/*namespace_id=*/2, /*fingerprint=*/103);
 
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/0,
-                 /*ref_namespace_fingerprint_ids=*/{id1}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id1}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/1,
-                 /*ref_namespace_fingerprint_ids=*/{id2}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id2}),
       IsOk());
   EXPECT_THAT(
       index->Put(schema_type_id, joinable_property_id, /*document_id=*/2,
-                 /*ref_namespace_fingerprint_ids=*/{id3}),
+                 /*ref_namespace_id_uri_fingerprints=*/{id3}),
       IsOk());
   index->set_last_added_document_id(3);
 
@@ -1386,10 +1370,10 @@
 }
 
 TEST_F(QualifiedIdJoinIndexImplV2Test, Clear) {
-  NamespaceFingerprintIdentifier id1(/*namespace_id=*/1, /*fingerprint=*/12);
-  NamespaceFingerprintIdentifier id2(/*namespace_id=*/1, /*fingerprint=*/34);
-  NamespaceFingerprintIdentifier id3(/*namespace_id=*/1, /*fingerprint=*/56);
-  NamespaceFingerprintIdentifier id4(/*namespace_id=*/1, /*fingerprint=*/78);
+  NamespaceIdFingerprint id1(/*namespace_id=*/1, /*fingerprint=*/12);
+  NamespaceIdFingerprint id2(/*namespace_id=*/1, /*fingerprint=*/34);
+  NamespaceIdFingerprint id3(/*namespace_id=*/1, /*fingerprint=*/56);
+  NamespaceIdFingerprint id4(/*namespace_id=*/1, /*fingerprint=*/78);
 
   // Create new qualified id join index
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1399,13 +1383,13 @@
   // Insert some data.
   ICING_ASSERT_OK(index->Put(
       /*schema_type_id=*/2, /*joinable_property_id=*/1, /*document_id=*/5,
-      /*ref_namespace_fingerprint_ids=*/{id2, id1}));
+      /*ref_namespace_id_uri_fingerprints=*/{id2, id1}));
   ICING_ASSERT_OK(index->Put(
       /*schema_type_id=*/3, /*joinable_property_id=*/10, /*document_id=*/6,
-      /*ref_namespace_fingerprint_ids=*/{id3}));
+      /*ref_namespace_id_uri_fingerprints=*/{id3}));
   ICING_ASSERT_OK(index->Put(
       /*schema_type_id=*/2, /*joinable_property_id=*/1, /*document_id=*/12,
-      /*ref_namespace_fingerprint_ids=*/{id4}));
+      /*ref_namespace_id_uri_fingerprints=*/{id4}));
   ASSERT_THAT(index, Pointee(SizeIs(4)));
   index->set_last_added_document_id(12);
   ASSERT_THAT(index->last_added_document_id(), Eq(12));
@@ -1425,22 +1409,21 @@
   // Join index should be able to work normally after Clear().
   ICING_ASSERT_OK(index->Put(
       /*schema_type_id=*/2, /*joinable_property_id=*/1, /*document_id=*/20,
-      /*ref_namespace_fingerprint_ids=*/{id4, id2, id1, id3}));
+      /*ref_namespace_id_uri_fingerprints=*/{id4, id2, id1, id3}));
   index->set_last_added_document_id(20);
 
   EXPECT_THAT(index, Pointee(SizeIs(4)));
   EXPECT_THAT(index->last_added_document_id(), Eq(20));
   EXPECT_THAT(
       GetJoinData(*index, /*schema_type_id=*/2, /*joinable_property_id=*/1),
-      IsOkAndHolds(
-          ElementsAre(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                          /*document_id=*/20, /*join_info=*/id4),
-                      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                          /*document_id=*/20, /*join_info=*/id3),
-                      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                          /*document_id=*/20, /*join_info=*/id2),
-                      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                          /*document_id=*/20, /*join_info=*/id1))));
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/20, /*join_info=*/id4),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/20, /*join_info=*/id3),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/20, /*join_info=*/id2),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/20, /*join_info=*/id1))));
 
   ICING_ASSERT_OK(index->PersistToDisk());
   index.reset();
@@ -1452,15 +1435,14 @@
   EXPECT_THAT(index->last_added_document_id(), Eq(20));
   EXPECT_THAT(
       GetJoinData(*index, /*schema_type_id=*/2, /*joinable_property_id=*/1),
-      IsOkAndHolds(
-          ElementsAre(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                          /*document_id=*/20, /*join_info=*/id4),
-                      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                          /*document_id=*/20, /*join_info=*/id3),
-                      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                          /*document_id=*/20, /*join_info=*/id2),
-                      DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-                          /*document_id=*/20, /*join_info=*/id1))));
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/20, /*join_info=*/id4),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/20, /*join_info=*/id3),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/20, /*join_info=*/id2),
+                               DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+                                   /*document_id=*/20, /*join_info=*/id1))));
 }
 
 }  // namespace
diff --git a/icing/join/qualified-id-join-index-impl-v3.cc b/icing/join/qualified-id-join-index-impl-v3.cc
new file mode 100644
index 0000000..bab844d
--- /dev/null
+++ b/icing/join/qualified-id-join-index-impl-v3.cc
@@ -0,0 +1,794 @@
+// Copyright (C) 2024 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/qualified-id-join-index-impl-v3.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#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/str_cat.h"
+#include "icing/feature-flags.h"
+#include "icing/file/destructible-directory.h"
+#include "icing/file/file-backed-vector.h"
+#include "icing/file/filesystem.h"
+#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/store/document-id.h"
+#include "icing/store/namespace-id.h"
+#include "icing/util/crc32.h"
+#include "icing/util/logging.h"
+#include "icing/util/math-util.h"
+#include "icing/util/status-macros.h"
+
+namespace icing {
+namespace lib {
+
+static constexpr DocumentJoinIdPair kInvalidDocumentJoinIdPair;
+static constexpr QualifiedIdJoinIndexImplV3::ArrayInfo kInvalidArrayInfo;
+
+namespace {
+
+std::string MakeMetadataFilePath(std::string_view working_path) {
+  return absl_ports::StrCat(working_path, "/metadata");
+}
+
+std::string MakeParentDocumentIdToChildArrayInfoFilePath(
+    std::string_view working_path) {
+  return absl_ports::StrCat(working_path,
+                            "/parent_document_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");
+}
+
+}  // namespace
+
+/* static */ libtextclassifier3::StatusOr<
+    std::unique_ptr<QualifiedIdJoinIndexImplV3>>
+QualifiedIdJoinIndexImplV3::Create(const Filesystem& filesystem,
+                                   std::string working_path,
+                                   const FeatureFlags& feature_flags) {
+  bool metadata_file_exists =
+      filesystem.FileExists(MakeMetadataFilePath(working_path).c_str());
+  bool parent_document_id_to_child_array_info_file_exists =
+      filesystem.FileExists(
+          MakeParentDocumentIdToChildArrayInfoFilePath(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 (metadata_file_exists &&
+      parent_document_id_to_child_array_info_file_exists &&
+      child_document_join_id_pair_array_file_exists) {
+    return InitializeExistingFiles(filesystem, std::move(working_path),
+                                   feature_flags);
+  }
+
+  // If all files don't exist, initialize new files.
+  if (!metadata_file_exists &&
+      !parent_document_id_to_child_array_info_file_exists &&
+      !child_document_join_id_pair_array_file_exists) {
+    return InitializeNewFiles(filesystem, std::move(working_path),
+                              feature_flags);
+  }
+
+  // Otherwise, some files are missing, and the join index is somehow corrupted.
+  // Return error to let the caller discard and rebuild.
+  return absl_ports::FailedPreconditionError(
+      "Inconsistent state of qualified id join index (v3)");
+}
+
+QualifiedIdJoinIndexImplV3::~QualifiedIdJoinIndexImplV3() {
+  if (!PersistToDisk().ok()) {
+    ICING_LOG(WARNING) << "Failed to persist qualified id join index (v3) to "
+                          "disk while destructing "
+                       << working_path_;
+  }
+}
+
+libtextclassifier3::Status QualifiedIdJoinIndexImplV3::Put(
+    const DocumentJoinIdPair& child_document_join_id_pair,
+    std::vector<DocumentId>&& parent_document_ids) {
+  if (!child_document_join_id_pair.is_valid()) {
+    return absl_ports::InvalidArgumentError("Invalid child DocumentJoinIdPair");
+  }
+
+  if (parent_document_ids.empty()) {
+    return libtextclassifier3::Status::OK;
+  }
+
+  SetDirty();
+
+  // Sort and dedupe parent document ids. It will be more efficient to access
+  // parent_document_id_to_child_array_info_ in the order of ids.
+  std::sort(parent_document_ids.begin(), parent_document_ids.end());
+  auto last =
+      std::unique(parent_document_ids.begin(), parent_document_ids.end());
+  parent_document_ids.erase(last, parent_document_ids.end());
+
+  // Append child_document_join_id_pair to each parent's DocumentJoinIdPair
+  // array.
+  for (DocumentId parent_document_id : parent_document_ids) {
+    if (parent_document_id < 0 || parent_document_id == kInvalidDocumentId) {
+      // Skip invalid parent document id.
+      continue;
+    }
+
+    // Insert child_document_join_id_pair into the parent's DocumentJoinIdPair
+    // array.
+    ICING_RETURN_IF_ERROR(AppendChildDocumentJoinIdPairsForParent(
+        parent_document_id, {child_document_join_id_pair}));
+  }
+  return libtextclassifier3::Status::OK;
+}
+
+libtextclassifier3::StatusOr<std::vector<DocumentJoinIdPair>>
+QualifiedIdJoinIndexImplV3::Get(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>();
+  }
+
+  // Get the child array info for the parent.
+  ICING_ASSIGN_OR_RETURN(
+      const ArrayInfo* array_info,
+      parent_document_id_to_child_array_info_->Get(parent_document_id));
+  if (!array_info->IsValid()) {
+    return std::vector<DocumentJoinIdPair>();
+  }
+
+  // Safe check to avoid out-of-bound access. This should never happen unless
+  // the internal data structure is corrupted.
+  if (array_info->index + array_info->length >
+      child_document_join_id_pair_array_->num_elements()) {
+    return absl_ports::InternalError(absl_ports::StrCat(
+        "Invalid array info: ", std::to_string(array_info->index), " + ",
+        std::to_string(array_info->length), " > ",
+        std::to_string(child_document_join_id_pair_array_->num_elements())));
+  }
+
+  // Get the DocumentJoinIdPair array and return the child DocumentJoinIdPairs.
+  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);
+}
+
+libtextclassifier3::Status QualifiedIdJoinIndexImplV3::MigrateParent(
+    DocumentId old_document_id, DocumentId new_document_id) {
+  if (!IsDocumentIdValid(old_document_id) ||
+      !IsDocumentIdValid(new_document_id)) {
+    return absl_ports::InvalidArgumentError(
+        "Invalid parent document id to migrate");
+  }
+
+  if (old_document_id >=
+      parent_document_id_to_child_array_info_->num_elements()) {
+    // It means the parent document doesn't have any children at this moment. No
+    // need to migrate.
+    return libtextclassifier3::Status::OK;
+  }
+
+  ICING_ASSIGN_OR_RETURN(
+      FileBackedVector<ArrayInfo>::MutableView mutable_old_array_info,
+      parent_document_id_to_child_array_info_->GetMutable(old_document_id));
+  if (!mutable_old_array_info.Get().IsValid()) {
+    // It means the parent document doesn't have any children at this moment. No
+    // need to migrate.
+    return libtextclassifier3::Status::OK;
+  }
+
+  ICING_RETURN_IF_ERROR(
+      ExtendParentDocumentIdToChildArrayInfoIfNecessary(new_document_id));
+  ICING_RETURN_IF_ERROR(parent_document_id_to_child_array_info_->Set(
+      new_document_id, mutable_old_array_info.Get()));
+  mutable_old_array_info.Get() = kInvalidArrayInfo;
+
+  return libtextclassifier3::Status::OK;
+}
+
+libtextclassifier3::Status QualifiedIdJoinIndexImplV3::Optimize(
+    const std::vector<DocumentId>& document_id_old_to_new,
+    const std::vector<NamespaceId>& namespace_id_old_to_new,
+    DocumentId new_last_added_document_id) {
+  std::string temp_working_path = working_path_ + "_temp";
+  ICING_RETURN_IF_ERROR(
+      QualifiedIdJoinIndex::Discard(filesystem_, temp_working_path));
+
+  DestructibleDirectory temp_working_path_ddir(&filesystem_,
+                                               std::move(temp_working_path));
+  if (!temp_working_path_ddir.is_valid()) {
+    return absl_ports::InternalError(
+        "Unable to create temp directory to build new qualified id join index "
+        "(v3)");
+  }
+
+  {
+    // Transfer all data from the current to new qualified id join index. Also
+    // PersistToDisk and destruct the instance after finishing, so we can safely
+    // swap directories later.
+    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()));
+    new_index->set_last_added_document_id(new_last_added_document_id);
+    new_index->SetDirty();
+
+    ICING_RETURN_IF_ERROR(new_index->PersistToDisk());
+  }
+
+  // Destruct current index's storage instances to safely swap directories.
+  child_document_join_id_pair_array_.reset();
+  parent_document_id_to_child_array_info_.reset();
+  metadata_mmapped_file_.reset();
+  if (!filesystem_.SwapFiles(temp_working_path_ddir.dir().c_str(),
+                             working_path_.c_str())) {
+    return absl_ports::InternalError(
+        "Unable to apply new qualified id join index (v3) due to failed swap");
+  }
+
+  // Reinitialize qualified id join index.
+  ICING_ASSIGN_OR_RETURN(
+      MemoryMappedFile metadata_mmapped_file,
+      MemoryMappedFile::Create(filesystem_, MakeMetadataFilePath(working_path_),
+                               MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC,
+                               /*max_file_size=*/kMetadataFileSize,
+                               /*pre_mapping_file_offset=*/0,
+                               /*pre_mapping_mmap_size=*/kMetadataFileSize));
+  metadata_mmapped_file_ =
+      std::make_unique<MemoryMappedFile>(std::move(metadata_mmapped_file));
+
+  ICING_ASSIGN_OR_RETURN(
+      parent_document_id_to_child_array_info_,
+      FileBackedVector<ArrayInfo>::Create(
+          filesystem_,
+          MakeParentDocumentIdToChildArrayInfoFilePath(working_path_),
+          MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC,
+          FileBackedVector<ArrayInfo>::kMaxFileSize,
+          /*pre_mapping_mmap_size=*/0));
+
+  ICING_ASSIGN_OR_RETURN(
+      child_document_join_id_pair_array_,
+      FileBackedVector<DocumentJoinIdPair>::Create(
+          filesystem_, MakeChildDocumentJoinIdPairArrayFilePath(working_path_),
+          MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC,
+          FileBackedVector<DocumentJoinIdPair>::kMaxFileSize,
+          /*pre_mapping_mmap_size=*/0));
+
+  return libtextclassifier3::Status::OK;
+}
+
+libtextclassifier3::Status QualifiedIdJoinIndexImplV3::Clear() {
+  SetDirty();
+
+  parent_document_id_to_child_array_info_.reset();
+  // Discard and reinitialize parent_document_id_to_child_array_info.
+  std::string parent_document_id_to_child_array_info_file_path =
+      MakeParentDocumentIdToChildArrayInfoFilePath(working_path_);
+  if (!filesystem_.DeleteFile(
+          parent_document_id_to_child_array_info_file_path.c_str())) {
+    return absl_ports::InternalError(absl_ports::StrCat(
+        "Failed to clear parent document id to child array info: ",
+        parent_document_id_to_child_array_info_file_path));
+  }
+  ICING_ASSIGN_OR_RETURN(
+      parent_document_id_to_child_array_info_,
+      FileBackedVector<ArrayInfo>::Create(
+          filesystem_, parent_document_id_to_child_array_info_file_path,
+          MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC,
+          FileBackedVector<ArrayInfo>::kMaxFileSize,
+          /*pre_mapping_mmap_size=*/0));
+
+  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 =
+      MakeChildDocumentJoinIdPairArrayFilePath(working_path_);
+  if (!filesystem_.DeleteFile(
+          child_document_join_id_pair_array_file_path.c_str())) {
+    return absl_ports::InternalError(absl_ports::StrCat(
+        "Failed to clear child document join id pair array: ",
+        child_document_join_id_pair_array_file_path));
+  }
+  ICING_ASSIGN_OR_RETURN(
+      child_document_join_id_pair_array_,
+      FileBackedVector<DocumentJoinIdPair>::Create(
+          filesystem_, child_document_join_id_pair_array_file_path,
+          MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC,
+          FileBackedVector<DocumentJoinIdPair>::kMaxFileSize,
+          /*pre_mapping_mmap_size=*/0));
+
+  info().num_data = 0;
+  info().last_added_document_id = kInvalidDocumentId;
+  return libtextclassifier3::Status::OK;
+}
+
+/* static */ libtextclassifier3::StatusOr<
+    std::unique_ptr<QualifiedIdJoinIndexImplV3>>
+QualifiedIdJoinIndexImplV3::InitializeNewFiles(
+    const Filesystem& filesystem, std::string working_path,
+    const FeatureFlags& feature_flags) {
+  // Create working directory.
+  if (!filesystem.CreateDirectoryRecursively(working_path.c_str())) {
+    return absl_ports::InternalError(
+        absl_ports::StrCat("Failed to create directory: ", working_path));
+  }
+
+  // Initialize metadata file.
+  // - Create MemoryMappedFile with pre-mapping and call GrowAndRemapIfNecessary
+  //   to grow the underlying file.
+  // - Initialize metadata content by writing 0 bytes to the memory region
+  //   directly.
+  ICING_ASSIGN_OR_RETURN(
+      MemoryMappedFile metadata_mmapped_file,
+      MemoryMappedFile::Create(filesystem, MakeMetadataFilePath(working_path),
+                               MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC,
+                               /*max_file_size=*/kMetadataFileSize,
+                               /*pre_mapping_file_offset=*/0,
+                               /*pre_mapping_mmap_size=*/kMetadataFileSize));
+  ICING_RETURN_IF_ERROR(metadata_mmapped_file.GrowAndRemapIfNecessary(
+      /*file_offset=*/0, /*mmap_size=*/kMetadataFileSize));
+  memset(metadata_mmapped_file.mutable_region(), 0, kMetadataFileSize);
+
+  // Initialize parent_document_id_to_child_array_info.
+  ICING_ASSIGN_OR_RETURN(
+      std::unique_ptr<FileBackedVector<ArrayInfo>>
+          parent_document_id_to_child_array_info,
+      FileBackedVector<ArrayInfo>::Create(
+          filesystem,
+          MakeParentDocumentIdToChildArrayInfoFilePath(working_path),
+          MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC,
+          FileBackedVector<ArrayInfo>::kMaxFileSize,
+          /*pre_mapping_mmap_size=*/0));
+
+  // Initialize child_document_join_id_pair_array.
+  ICING_ASSIGN_OR_RETURN(
+      std::unique_ptr<FileBackedVector<DocumentJoinIdPair>>
+          child_document_join_id_pair_array,
+      FileBackedVector<DocumentJoinIdPair>::Create(
+          filesystem, MakeChildDocumentJoinIdPairArrayFilePath(working_path),
+          MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC,
+          FileBackedVector<DocumentJoinIdPair>::kMaxFileSize,
+          /*pre_mapping_mmap_size=*/0));
+
+  // Create instance.
+  auto new_join_index = std::unique_ptr<QualifiedIdJoinIndexImplV3>(
+      new QualifiedIdJoinIndexImplV3(
+          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(child_document_join_id_pair_array), feature_flags));
+  // Initialize info content.
+  new_join_index->info().magic = Info::kMagic;
+  new_join_index->info().num_data = 0;
+  new_join_index->info().last_added_document_id = kInvalidDocumentId;
+
+  // Initialize new PersistentStorage. The initial checksums will be computed
+  // and set via InitializeNewStorage.
+  ICING_RETURN_IF_ERROR(new_join_index->InitializeNewStorage());
+
+  return new_join_index;
+}
+
+/* static */ libtextclassifier3::StatusOr<
+    std::unique_ptr<QualifiedIdJoinIndexImplV3>>
+QualifiedIdJoinIndexImplV3::InitializeExistingFiles(
+    const Filesystem& filesystem, std::string working_path,
+    const FeatureFlags& feature_flags) {
+  // Initialize metadata file.
+  ICING_ASSIGN_OR_RETURN(
+      MemoryMappedFile metadata_mmapped_file,
+      MemoryMappedFile::Create(filesystem, MakeMetadataFilePath(working_path),
+                               MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC,
+                               /*max_file_size=*/kMetadataFileSize,
+                               /*pre_mapping_file_offset=*/0,
+                               /*pre_mapping_mmap_size=*/kMetadataFileSize));
+  if (metadata_mmapped_file.available_size() != kMetadataFileSize) {
+    return absl_ports::FailedPreconditionError("Incorrect metadata file size");
+  }
+
+  // Initialize parent_document_id_to_child_array_info. Set mmap pre-mapping
+  // size to 0, but MemoryMappedFile will still mmap to the file size.
+  ICING_ASSIGN_OR_RETURN(
+      std::unique_ptr<FileBackedVector<ArrayInfo>>
+          parent_document_id_to_child_array_info,
+      FileBackedVector<ArrayInfo>::Create(
+          filesystem,
+          MakeParentDocumentIdToChildArrayInfoFilePath(working_path),
+          MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC,
+          FileBackedVector<ArrayInfo>::kMaxFileSize,
+          /*pre_mapping_mmap_size=*/0));
+
+  // 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(
+      std::unique_ptr<FileBackedVector<DocumentJoinIdPair>>
+          child_document_join_id_pair_array,
+      FileBackedVector<DocumentJoinIdPair>::Create(
+          filesystem, MakeChildDocumentJoinIdPairArrayFilePath(working_path),
+          MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC,
+          FileBackedVector<DocumentJoinIdPair>::kMaxFileSize,
+          /*pre_mapping_mmap_size=*/0));
+
+  // Create instance.
+  auto join_index = std::unique_ptr<QualifiedIdJoinIndexImplV3>(
+      new QualifiedIdJoinIndexImplV3(
+          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(child_document_join_id_pair_array), feature_flags));
+
+  // Initialize existing PersistentStorage. Checksums will be validated.
+  ICING_RETURN_IF_ERROR(join_index->InitializeExistingStorage());
+
+  // Validate magic.
+  if (join_index->info().magic != Info::kMagic) {
+    return absl_ports::FailedPreconditionError("Incorrect magic value");
+  }
+
+  return join_index;
+}
+
+libtextclassifier3::Status
+QualifiedIdJoinIndexImplV3::AppendChildDocumentJoinIdPairsForParent(
+    DocumentId parent_document_id,
+    std::vector<DocumentJoinIdPair>&& child_document_join_id_pairs) {
+  if (child_document_join_id_pairs.empty()) {
+    return libtextclassifier3::Status::OK;
+  }
+
+  // Step 1: extend parent_document_id_to_child_array_info_ if necessary.
+  ICING_RETURN_IF_ERROR(
+      ExtendParentDocumentIdToChildArrayInfoIfNecessary(parent_document_id));
+
+  // Step 2: get the parent's child DocumentJoinIdPair mutable array (extend if
+  //         necessary), append new child_document_join_id_pairs to the array,
+  //         and assign the new ArrayInfo to
+  //         parent_document_id_to_child_array_info_.
+  ICING_ASSIGN_OR_RETURN(
+      const ArrayInfo* array_info,
+      parent_document_id_to_child_array_info_->Get(parent_document_id));
+  ICING_ASSIGN_OR_RETURN(
+      GetMutableAndExtendResult result,
+      GetMutableAndExtendChildDocumentJoinIdPairArrayIfNecessary(
+          *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.
+  result.mutable_arr.SetArray(/*idx=*/result.array_info.used_length,
+                              /*arr=*/child_document_join_id_pairs.data(),
+                              /*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 libtextclassifier3::Status::OK;
+}
+
+libtextclassifier3::Status
+QualifiedIdJoinIndexImplV3::ExtendParentDocumentIdToChildArrayInfoIfNecessary(
+    DocumentId parent_document_id) {
+  if (parent_document_id >=
+      parent_document_id_to_child_array_info_->num_elements()) {
+    int32_t num_to_extend =
+        (parent_document_id + 1) -
+        parent_document_id_to_child_array_info_->num_elements();
+    ICING_ASSIGN_OR_RETURN(
+        FileBackedVector<ArrayInfo>::MutableArrayView mutable_arr,
+        parent_document_id_to_child_array_info_->Allocate(num_to_extend));
+    mutable_arr.Fill(/*idx=*/0, /*len=*/num_to_extend, kInvalidArrayInfo);
+  }
+  return libtextclassifier3::Status::OK;
+}
+
+libtextclassifier3::StatusOr<
+    QualifiedIdJoinIndexImplV3::GetMutableAndExtendResult>
+QualifiedIdJoinIndexImplV3::
+    GetMutableAndExtendChildDocumentJoinIdPairArrayIfNecessary(
+        const ArrayInfo& array_info, int32_t num_to_add) {
+  if (num_to_add > kMaxNumChildrenPerParent) {
+    return absl_ports::ResourceExhaustedError(
+        absl_ports::StrCat("Too many children to add for a single parent: ",
+                           std::to_string(num_to_add)));
+  }
+
+  if (!array_info.IsValid()) {
+    // If the array info is invalid, then it means the array is not allocated
+    // yet. Allocate a new array and fill it with invalid values.
+    ArrayInfo new_array_info(
+        /*index_in=*/child_document_join_id_pair_array_->num_elements(),
+        /*length_in=*/math_util::NextPowerOf2(num_to_add),
+        /*used_length_in=*/0);
+    ICING_ASSIGN_OR_RETURN(
+        FileBackedVector<DocumentJoinIdPair>::MutableArrayView new_mutable_arr,
+        child_document_join_id_pair_array_->Allocate(new_array_info.length));
+    new_mutable_arr.Fill(/*idx=*/0, /*len=*/new_array_info.length,
+                         kInvalidDocumentJoinIdPair);
+
+    return GetMutableAndExtendResult{.array_info = new_array_info,
+                                     .mutable_arr = std::move(new_mutable_arr)};
+  }
+
+  // The original array is valid. Get it and check if it needs to be extended.
+  ICING_ASSIGN_OR_RETURN(FileBackedVector<DocumentJoinIdPair>::MutableArrayView
+                             original_mutable_arr,
+                         child_document_join_id_pair_array_->GetMutable(
+                             array_info.index, array_info.length));
+  // - [0, array_info.used_length) contain valid elements.
+  // - [array_info.used_length, array_info.length) are invalid elements and
+  //   available for new elements.
+  if (array_info.length - array_info.used_length >= num_to_add) {
+    // Available space is enough to fit num_to_add. Return the original array.
+    return GetMutableAndExtendResult{
+        .array_info = array_info,
+        .mutable_arr = std::move(original_mutable_arr)};
+  }
+
+  // Need to extend. The new array length should be able to fit all existing
+  // elements and the newly added elements.
+  //
+  // Check the new # of elements should not exceed the max limit. Use
+  // substraction to avoid overflow.
+  if (num_to_add > kMaxNumChildrenPerParent - array_info.used_length) {
+    return absl_ports::ResourceExhaustedError(absl_ports::StrCat(
+        "Too many children to add for an existing parent: ",
+        std::to_string(num_to_add),
+        "; existing: ", std::to_string(array_info.used_length)));
+  }
+
+  if (array_info.index + array_info.length ==
+      child_document_join_id_pair_array_->num_elements()) {
+    // The original array is at the end of the FBV. We can extend and reshape it
+    // directly without copying elements.
+    //
+    // (Note: # means data out of the array_info we're dealing with.)
+    // Original FBV and array_info layout:
+    // +-------------------------+---------------.------+
+    // |#########################|     used      |unused|
+    // +-------------------------+---------------.------+
+    //                           ^                      ^
+    //                         index                 FBV_END
+    //                           |<-used_length->|
+    //                           |<-      length      ->|
+    //
+    // New FBV and new_array_info layout after extension:
+    // +-------------------------+---------------.------.------------------+
+    // |#########################|     used      |unused|  extended slice  |
+    // +-------------------------+---------------.------.------------------+
+    //                           ^                                         ^
+    //                         index                                    FBV_END
+    //                           |<-used_length->|
+    //                           |<-              new length             ->|
+    ArrayInfo new_array_info = array_info;
+    new_array_info.length =
+        math_util::NextPowerOf2(array_info.used_length + num_to_add);
+
+    // Extend the original array by allocating a new slice.
+    ICING_ASSIGN_OR_RETURN(
+        FileBackedVector<DocumentJoinIdPair>::MutableArrayView extended_slice,
+        child_document_join_id_pair_array_->Allocate(new_array_info.length -
+                                                     array_info.length));
+    // Fill the extended slice with invalid values.
+    extended_slice.Fill(/*idx=*/0,
+                        /*len=*/new_array_info.length - array_info.length,
+                        kInvalidDocumentJoinIdPair);
+
+    // Construct the mutable array view for new_array_info and return.
+    ICING_ASSIGN_OR_RETURN(
+        FileBackedVector<DocumentJoinIdPair>::MutableArrayView mutable_arr,
+        child_document_join_id_pair_array_->GetMutable(new_array_info.index,
+                                                       new_array_info.length));
+
+    return GetMutableAndExtendResult{.array_info = std::move(new_array_info),
+                                     .mutable_arr = std::move(mutable_arr)};
+  }
+
+  // The original array is not at the end of the file. We allocate a new array
+  // starting at the end of the FBV, copy all existing elements to the new
+  // array, and fill the remaining with invalid values.
+  //
+  // Original FBV and array_info layout:
+  // +---+---------------.------+------+
+  // |###|     used      |unused|######|
+  // +---+---------------.------+------+
+  //     ^                             ^
+  //   index                        FBV_END
+  //     |<-used_length->|
+  //     |<-      length      ->|
+  //
+  // New FBV and new_array_info layout after extension and moving:
+  // +---+----------------------+------+---------------.-----------------------+
+  // |###|      <INVALID>       |######|     used      |                       |
+  // +---+----------------------+------+---------------.-----------------------+
+  //                                   ^
+  //                               new_index
+  //                                   |<-used_length->|
+  //                                   |<-            new_length             ->|
+  ArrayInfo new_array_info(
+      /*index_in=*/child_document_join_id_pair_array_->num_elements(),
+      /*length_in=*/
+      math_util::NextPowerOf2(array_info.used_length + num_to_add),
+      /*used_length_in=*/array_info.used_length);
+  ICING_ASSIGN_OR_RETURN(
+      FileBackedVector<DocumentJoinIdPair>::MutableArrayView new_mutable_arr,
+      child_document_join_id_pair_array_->Allocate(new_array_info.length));
+
+  // It is possible that Allocate() causes FBV to grow and remap. In this case,
+  // original_mutable_arr will point to an invalid memory region, so we need to
+  // refresh it.
+  //
+  // Note: original_mutable_arr has length = array_info.length and only contains
+  //   valid elements with length = array_info.used_length. We could've
+  //   constructed original_mutable_arr with length = array_info.used_length
+  //   here, but let's make it consistent with all other cases (i.e.
+  //   constructing the array view object for the entire extensible array).
+  ICING_ASSIGN_OR_RETURN(original_mutable_arr,
+                         child_document_join_id_pair_array_->GetMutable(
+                             array_info.index, array_info.length));
+
+  // Move all existing elements to the new array.
+  new_mutable_arr.SetArray(/*idx=*/0, original_mutable_arr.data(),
+                           array_info.used_length);
+  // Fill the remaining with invalid values.
+  new_mutable_arr.Fill(/*idx=*/array_info.used_length,
+                       /*len=*/new_array_info.length - array_info.used_length,
+                       kInvalidDocumentJoinIdPair);
+  // Invalidate the original array.
+  original_mutable_arr.Fill(
+      /*idx=*/0, /*len=*/array_info.length, kInvalidDocumentJoinIdPair);
+  return GetMutableAndExtendResult{.array_info = new_array_info,
+                                   .mutable_arr = std::move(new_mutable_arr)};
+}
+
+libtextclassifier3::Status QualifiedIdJoinIndexImplV3::TransferIndex(
+    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 >= document_id_old_to_new.size() ||
+        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();
+      DocumentId new_child_doc_id =
+          old_child_doc_id >= 0 &&
+                  old_child_doc_id < document_id_old_to_new.size()
+              ? document_id_old_to_new[old_child_doc_id]
+              : kInvalidDocumentId;
+      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()));
+    }
+
+    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;
+}
+
+libtextclassifier3::Status QualifiedIdJoinIndexImplV3::PersistMetadataToDisk() {
+  // We can skip persisting metadata to disk only if both info and storage are
+  // clean.
+  if (is_initialized_ && !is_info_dirty() && !is_storage_dirty()) {
+    return libtextclassifier3::Status::OK;
+  }
+
+  // Changes should have been applied to the underlying file when using
+  // MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC, but call msync() as an
+  // extra safety step to ensure they are written out.
+  ICING_RETURN_IF_ERROR(metadata_mmapped_file_->PersistToDisk());
+  is_info_dirty_ = false;
+  return libtextclassifier3::Status::OK;
+}
+
+libtextclassifier3::Status QualifiedIdJoinIndexImplV3::PersistStoragesToDisk() {
+  if (is_initialized_ && !is_storage_dirty()) {
+    return libtextclassifier3::Status::OK;
+  }
+
+  ICING_RETURN_IF_ERROR(
+      parent_document_id_to_child_array_info_->PersistToDisk());
+  ICING_RETURN_IF_ERROR(child_document_join_id_pair_array_->PersistToDisk());
+
+  is_storage_dirty_ = false;
+  return libtextclassifier3::Status::OK;
+}
+
+libtextclassifier3::StatusOr<Crc32>
+QualifiedIdJoinIndexImplV3::UpdateStoragesChecksum() {
+  if (is_initialized_ && !is_storage_dirty()) {
+    return Crc32(crcs().component_crcs.storages_crc);
+  }
+
+  // Compute crcs
+  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 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() ^
+               child_document_join_id_pair_array_crc.Get());
+}
+
+libtextclassifier3::StatusOr<Crc32>
+QualifiedIdJoinIndexImplV3::GetInfoChecksum() const {
+  if (is_initialized_ && !is_info_dirty()) {
+    return Crc32(crcs().component_crcs.info_crc);
+  }
+
+  return info().GetChecksum();
+}
+
+libtextclassifier3::StatusOr<Crc32>
+QualifiedIdJoinIndexImplV3::GetStoragesChecksum() const {
+  if (is_initialized_ && !is_storage_dirty()) {
+    return Crc32(crcs().component_crcs.storages_crc);
+  }
+
+  // Get checksums for all components.
+  Crc32 parent_document_id_to_child_array_info_crc =
+      parent_document_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() ^
+               child_document_join_id_pair_array_crc.Get());
+}
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/join/qualified-id-join-index-impl-v3.h b/icing/join/qualified-id-join-index-impl-v3.h
new file mode 100644
index 0000000..2f5a8a5
--- /dev/null
+++ b/icing/join/qualified-id-join-index-impl-v3.h
@@ -0,0 +1,414 @@
+// Copyright (C) 2024 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_QUALIFIED_ID_JOIN_INDEX_IMPL_V3_H_
+#define ICING_JOIN_QUALIFIED_ID_JOIN_INDEX_IMPL_V3_H_
+
+#include <cstdint>
+#include <memory>
+#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/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/join/document-join-id-pair.h"
+#include "icing/join/qualified-id-join-index.h"
+#include "icing/schema/joinable-property.h"
+#include "icing/store/document-filter-data.h"
+#include "icing/store/document-id.h"
+#include "icing/store/namespace-id-fingerprint.h"
+#include "icing/store/namespace-id.h"
+#include "icing/util/crc32.h"
+
+namespace icing {
+namespace lib {
+
+// QualifiedIdJoinIndexImplV3: a class to maintain join data. It uses 2
+// FileBackedVectors to maintain the mapping between parent document id and a
+// list of its joinable child infos (document id, joinable property id).
+class QualifiedIdJoinIndexImplV3 : public QualifiedIdJoinIndex {
+ public:
+  struct Info {
+    static constexpr int32_t kMagic = 0x1529a926;
+
+    int32_t magic;
+    int32_t num_data;
+    DocumentId last_added_document_id;
+
+    // Padding exists just to reserve space for additional values and make the
+    // size of the metadata file 1024 bytes.
+    static constexpr int kPaddingSize = 1000;
+    uint8_t padding[kPaddingSize];
+
+    Crc32 GetChecksum() const {
+      return Crc32(
+          std::string_view(reinterpret_cast<const char*>(this), sizeof(Info)));
+    }
+  } __attribute__((packed));
+  static_assert(sizeof(Info) == 1012, "");
+
+  // Metadata file layout: <Crcs><Info<data><padding>>
+  static constexpr int32_t kCrcsMetadataFileOffset = 0;
+  static constexpr int32_t kInfoMetadataFileOffset =
+      static_cast<int32_t>(sizeof(Crcs));
+  static constexpr int32_t kMetadataFileSize = sizeof(Crcs) + sizeof(Info);
+  static_assert(kMetadataFileSize == 1024, "");
+
+  static constexpr WorkingPathType kWorkingPathType =
+      WorkingPathType::kDirectory;
+
+  // An internal struct to store the start index and length of a
+  // DocumentJoinIdPair array in child_document_join_id_pair_array_.
+  struct ArrayInfo {
+    int32_t index;
+    int32_t length;
+    int32_t used_length;
+
+    constexpr ArrayInfo() : index(-1), length(-1), used_length(-1) {}
+
+    explicit ArrayInfo(int32_t index_in, int32_t length_in,
+                       int32_t used_length_in)
+        : index(index_in), length(length_in), used_length(used_length_in) {}
+
+    bool operator==(const ArrayInfo& other) const {
+      return index == other.index && length == other.length &&
+             used_length == other.used_length;
+    }
+
+    bool IsValid() const {
+      return index >= 0 && length > 0 && used_length >= 0 &&
+             used_length <= length;
+    }
+  } __attribute__((packed));
+  static_assert(sizeof(ArrayInfo) == 12, "Invalid ArrayInfo size");
+
+  // Creates a QualifiedIdJoinIndexImplV3 instance to store parent document to a
+  // list of its joinable children for future joining search. If any of the
+  // underlying file is missing, then delete the whole working_path and
+  // (re)initialize with new ones. Otherwise initialize and create the instance
+  // by existing files.
+  //
+  // filesystem: Object to make system level calls
+  // working_path: Specifies the working path for PersistentStorage.
+  //               QualifiedIdJoinIndexImplV3 uses working path as working
+  //               directory and all related files will be stored under this
+  //               directory. It takes full ownership and of working_path_,
+  //               including creation/deletion. It is the caller's
+  //               responsibility to specify correct working path and avoid
+  //               mixing different persistent storages together under the same
+  //               path. Also the caller has the ownership for the parent
+  //               directory of working_path_, and it is responsible for parent
+  //               directory creation/deletion. See PersistentStorage for more
+  //               details about the concept of working_path.
+  // feature_flags: a reference to (global) icing search engine feature flags.
+  //
+  // Returns:
+  //   - FAILED_PRECONDITION_ERROR if the state of the join index is
+  //                               inconsistent (e.g. checksum or the magic
+  //                               doesn't match the stored ones, missing some
+  //                               files, etc).
+  //   - INTERNAL_ERROR on I/O errors
+  //   - Any MemoryMappedFile, FileBackedVector errors
+  static libtextclassifier3::StatusOr<
+      std::unique_ptr<QualifiedIdJoinIndexImplV3>>
+  Create(const Filesystem& filesystem, std::string working_path,
+         const FeatureFlags& feature_flags);
+
+  // Delete copy and move constructor/assignment operator.
+  QualifiedIdJoinIndexImplV3(const QualifiedIdJoinIndexImplV3&) = delete;
+  QualifiedIdJoinIndexImplV3& operator=(const QualifiedIdJoinIndexImplV3&) =
+      delete;
+
+  QualifiedIdJoinIndexImplV3(QualifiedIdJoinIndexImplV3&&) = delete;
+  QualifiedIdJoinIndexImplV3& operator=(QualifiedIdJoinIndexImplV3&&) = delete;
+
+  ~QualifiedIdJoinIndexImplV3() override;
+
+  // 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
+  //   - INVALID_ARGUMENT_ERROR if child_document_join_id_pair is invalid
+  //   - Any FileBackedVector errors
+  libtextclassifier3::Status Put(
+      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.
+  //
+  // Returns:
+  //   - A list of children's DocumentJoinIdPair on success
+  //   - Any FileBackedVector errors
+  libtextclassifier3::StatusOr<std::vector<DocumentJoinIdPair>> Get(
+      DocumentId parent_document_id) const override;
+
+  // Migrates existing join data for a parent document from old_document_id to
+  // new_document_id.
+  //
+  // Note: when updating a document, we have to migrate the document's join data
+  // if it is used as a parent document. For its child join data, it will be
+  // tokenized and indexed separately, so no migration is needed.
+  //
+  // Returns:
+  //   - OK on success
+  //   - INVALID_ARGUMENT_ERROR if any document id is invalid
+  //   - Any FileBackedVector errors
+  libtextclassifier3::Status MigrateParent(DocumentId old_document_id,
+                                           DocumentId new_document_id) override;
+
+  // v1 only API. Returns UNIMPLEMENTED_ERROR.
+  libtextclassifier3::Status Put(
+      const DocumentJoinIdPair& document_join_id_pair,
+      std::string_view ref_qualified_id_str) override {
+    return absl_ports::UnimplementedError("This API is not supported in V3");
+  }
+
+  // v1 only API. Returns UNIMPLEMENTED_ERROR.
+  libtextclassifier3::StatusOr<std::string_view> Get(
+      const DocumentJoinIdPair& document_join_id_pair) const override {
+    return absl_ports::UnimplementedError("This API is not supported in V3");
+  }
+
+  // v2 only API. Returns UNIMPLEMENTED_ERROR.
+  libtextclassifier3::Status Put(
+      SchemaTypeId schema_type_id, JoinablePropertyId joinable_property_id,
+      DocumentId document_id,
+      std::vector<NamespaceIdFingerprint>&& ref_namespace_id_uri_fingerprints)
+      override {
+    return absl_ports::UnimplementedError("This API is not supported in V3");
+  }
+
+  // v2 only API. Returns UNIMPLEMENTED_ERROR.
+  libtextclassifier3::StatusOr<std::unique_ptr<JoinDataIteratorBase>>
+  GetIterator(SchemaTypeId schema_type_id,
+              JoinablePropertyId joinable_property_id) const override {
+    return absl_ports::UnimplementedError("This API is not supported in V3");
+  }
+
+  libtextclassifier3::Status Optimize(
+      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;
+
+  libtextclassifier3::Status Clear() override;
+
+  QualifiedIdJoinIndex::Version version() const override {
+    return QualifiedIdJoinIndex::Version::kV3;
+  }
+
+  int32_t size() const override { return info().num_data; }
+
+  bool empty() const override { return size() == 0; }
+
+  DocumentId last_added_document_id() const override {
+    return info().last_added_document_id;
+  }
+
+  void set_last_added_document_id(DocumentId document_id) override {
+    SetInfoDirty();
+
+    Info& info_ref = info();
+    if (info_ref.last_added_document_id == kInvalidDocumentId ||
+        document_id > info_ref.last_added_document_id) {
+      info_ref.last_added_document_id = document_id;
+    }
+  }
+
+ private:
+  // Set max # of child DocumentJoinIdPair per parent to 2^16.
+  // - It is unlikely that a single parent document will have extremely large
+  //   # of children.
+  // - Also prevent over extending the array.
+  static constexpr int32_t kMaxNumChildrenPerParent = INT32_C(1) << 16;
+  static_assert(kMaxNumChildrenPerParent <= (1u << 31),
+                "Required for math_util::NextPowerOf2");
+
+  explicit QualifiedIdJoinIndexImplV3(
+      const Filesystem& filesystem, std::string working_path,
+      std::unique_ptr<MemoryMappedFile> metadata_mmapped_file,
+      std::unique_ptr<FileBackedVector<ArrayInfo>>
+          parent_document_id_to_child_array_info,
+      std::unique_ptr<FileBackedVector<DocumentJoinIdPair>>
+          child_document_join_id_pair_array,
+      const FeatureFlags& feature_flags)
+      : QualifiedIdJoinIndex(filesystem, std::move(working_path)),
+        metadata_mmapped_file_(std::move(metadata_mmapped_file)),
+        parent_document_id_to_child_array_info_(
+            std::move(parent_document_id_to_child_array_info)),
+        child_document_join_id_pair_array_(
+            std::move(child_document_join_id_pair_array)),
+        feature_flags_(feature_flags),
+        is_info_dirty_(false),
+        is_storage_dirty_(false) {}
+
+  static libtextclassifier3::StatusOr<
+      std::unique_ptr<QualifiedIdJoinIndexImplV3>>
+  InitializeNewFiles(const Filesystem& filesystem, std::string working_path,
+                     const FeatureFlags& feature_flags);
+
+  static libtextclassifier3::StatusOr<
+      std::unique_ptr<QualifiedIdJoinIndexImplV3>>
+  InitializeExistingFiles(const Filesystem& filesystem,
+                          std::string working_path,
+                          const FeatureFlags& feature_flags);
+
+  // 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.
+  //
+  // Returns:
+  //   - OK on success
+  //   - RESOURCE_EXHAUSTED_ERROR if the new # of elements exceed
+  //     kMaxNumChildrenPerParent
+  //   - Any FileBackedVector errors
+  libtextclassifier3::Status AppendChildDocumentJoinIdPairsForParent(
+      DocumentId parent_document_id,
+      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.
+  //
+  // Returns:
+  //   - OK on success
+  //   - Any FileBackedVector errors
+  libtextclassifier3::Status ExtendParentDocumentIdToChildArrayInfoIfNecessary(
+      DocumentId parent_document_id);
+
+  // Gets the DocumentJoinIdPair mutable array and extends it if necessary to
+  // fit num_to_add new elements in the future for the caller. If extended:
+  // - Allocate a new array with size rounded up to the next 2's power to fit
+  //   all existing elements and num_to_add new elements.
+  // - Old elements will be migrated to the new array.
+  // - The old array will be invalidated.
+  //
+  // Note: this function only prepares the array to fit "future" num_to_add
+  // elements for the caller. It potentially does the extension but without
+  // adding any new elements into the array, so the returned ArrayInfo's
+  // used_length will remain unchanged.
+  //
+  // Returns:
+  //   - GetOrExtendResult on success
+  //   - RESOURCE_EXHAUSTED_ERROR if the new # of elements exceed
+  //     kMaxNumChildrenPerParent
+  //   - Any FileBackedVector errors
+  struct GetMutableAndExtendResult {
+    // ArrayInfo of the DocumentJoinIdPair array.
+    // - If extended (allocated a new array), then the new array's ArrayInfo
+    //   will be returned.
+    // - Otherwise, the input (original) ArrayInfo will be returned.
+    ArrayInfo array_info;
+
+    // MutableArrayView object of the DocumentJoinIdPair array corresponding to
+    // array_info. It will be either the new array after extension or the
+    // original array, depending on whether it is extended or not.
+    FileBackedVector<DocumentJoinIdPair>::MutableArrayView mutable_arr;
+  };
+  libtextclassifier3::StatusOr<GetMutableAndExtendResult>
+  GetMutableAndExtendChildDocumentJoinIdPairArrayIfNecessary(
+      const ArrayInfo& array_info, int32_t num_to_add);
+
+  // Transfers qualified id join index data from the current to new_index and
+  // convert to new document id according to document_id_old_to_new. It is a
+  // helper function for Optimize.
+  //
+  // Returns:
+  //   - OK on success
+  //   - INTERNAL_ERROR on I/O error
+  libtextclassifier3::Status TransferIndex(
+      const std::vector<DocumentId>& document_id_old_to_new,
+      QualifiedIdJoinIndexImplV3* new_index) const;
+
+  libtextclassifier3::Status PersistMetadataToDisk() override;
+
+  libtextclassifier3::Status PersistStoragesToDisk() override;
+
+  libtextclassifier3::Status WriteMetadata() override {
+    // QualifiedIdJoinIndexImplV3::Header is mmapped. Therefore, writes occur
+    // when the metadata is modified. So just return OK.
+    return libtextclassifier3::Status::OK;
+  }
+
+  libtextclassifier3::StatusOr<Crc32> UpdateStoragesChecksum() override;
+
+  libtextclassifier3::StatusOr<Crc32> GetInfoChecksum() const override;
+
+  libtextclassifier3::StatusOr<Crc32> GetStoragesChecksum() const override;
+
+  Crcs& crcs() override {
+    return *reinterpret_cast<Crcs*>(metadata_mmapped_file_->mutable_region() +
+                                    kCrcsMetadataFileOffset);
+  }
+
+  const Crcs& crcs() const override {
+    return *reinterpret_cast<const Crcs*>(metadata_mmapped_file_->region() +
+                                          kCrcsMetadataFileOffset);
+  }
+
+  Info& info() {
+    return *reinterpret_cast<Info*>(metadata_mmapped_file_->mutable_region() +
+                                    kInfoMetadataFileOffset);
+  }
+
+  const Info& info() const {
+    return *reinterpret_cast<const Info*>(metadata_mmapped_file_->region() +
+                                          kInfoMetadataFileOffset);
+  }
+
+  void SetInfoDirty() { is_info_dirty_ = true; }
+  // When storage is dirty, we have to set info dirty as well. So just expose
+  // SetDirty to set both.
+  void SetDirty() {
+    is_info_dirty_ = true;
+    is_storage_dirty_ = true;
+  }
+
+  bool is_info_dirty() const { return is_info_dirty_; }
+  bool is_storage_dirty() const { return is_storage_dirty_; }
+
+  std::unique_ptr<MemoryMappedFile> metadata_mmapped_file_;
+
+  // Storage for mapping parent document id to the ArrayInfo, which points to an
+  // extensible array stored in child_document_join_id_pair_array_, and the
+  // extensible array contains the parent's joinable children information.
+  std::unique_ptr<FileBackedVector<ArrayInfo>>
+      parent_document_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.
+  // - The extensible array information (starting index of the FileBackedVector,
+  //   length) is stored in parent_document_id_to_child_array_info_.
+  std::unique_ptr<FileBackedVector<DocumentJoinIdPair>>
+      child_document_join_id_pair_array_;
+
+  const FeatureFlags& feature_flags_;  // Does not own.
+
+  bool is_info_dirty_;
+  bool is_storage_dirty_;
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_JOIN_QUALIFIED_ID_JOIN_INDEX_IMPL_V3_H_
diff --git a/icing/join/qualified-id-join-index-impl-v3_test.cc b/icing/join/qualified-id-join-index-impl-v3_test.cc
new file mode 100644
index 0000000..529100b
--- /dev/null
+++ b/icing/join/qualified-id-join-index-impl-v3_test.cc
@@ -0,0 +1,1388 @@
+// Copyright (C) 2024 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/qualified-id-join-index-impl-v3.h"
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.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"
+#include "icing/file/memory-mapped-file.h"
+#include "icing/file/persistent-storage.h"
+#include "icing/join/document-join-id-pair.h"
+#include "icing/store/document-id.h"
+#include "icing/testing/common-matchers.h"
+#include "icing/testing/test-feature-flags.h"
+#include "icing/testing/tmp-directory.h"
+#include "icing/util/crc32.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::Lt;
+using ::testing::Not;
+using ::testing::Pointee;
+using ::testing::SizeIs;
+
+using Crcs = PersistentStorage::Crcs;
+using Info = QualifiedIdJoinIndexImplV3::Info;
+using ArrayInfo = QualifiedIdJoinIndexImplV3::ArrayInfo;
+
+class QualifiedIdJoinIndexImplV3Test : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+
+    base_dir_ = GetTestTempDir() + "/icing";
+    ASSERT_THAT(filesystem_.CreateDirectoryRecursively(base_dir_.c_str()),
+                IsTrue());
+
+    working_path_ = base_dir_ + "/qualified_id_join_index_impl_v3";
+  }
+
+  void TearDown() override {
+    filesystem_.DeleteDirectoryRecursively(base_dir_.c_str());
+  }
+
+  std::unique_ptr<FeatureFlags> feature_flags_;
+  Filesystem filesystem_;
+  std::string base_dir_;
+  std::string working_path_;
+};
+
+TEST_F(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) {
+  {
+    // Create new qualified id join index
+    ASSERT_FALSE(filesystem_.DirectoryExists(working_path_.c_str()));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+        QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                           *feature_flags_));
+    EXPECT_THAT(index, Pointee(IsEmpty()));
+
+    ICING_ASSERT_OK(index->PersistToDisk());
+  }
+
+  // Metadata file should be initialized correctly for both info and crcs
+  // sections.
+  const std::string metadata_file_path =
+      absl_ports::StrCat(working_path_, "/metadata");
+  auto metadata_buffer = std::make_unique<uint8_t[]>(
+      QualifiedIdJoinIndexImplV3::kMetadataFileSize);
+  ASSERT_THAT(
+      filesystem_.PRead(metadata_file_path.c_str(), metadata_buffer.get(),
+                        QualifiedIdJoinIndexImplV3::kMetadataFileSize,
+                        /*offset=*/0),
+      IsTrue());
+
+  // Check info section
+  const Info* info = reinterpret_cast<const Info*>(
+      metadata_buffer.get() +
+      QualifiedIdJoinIndexImplV3::kInfoMetadataFileOffset);
+  EXPECT_THAT(info->magic, Eq(Info::kMagic));
+  EXPECT_THAT(info->num_data, Eq(0));
+  EXPECT_THAT(info->last_added_document_id, Eq(kInvalidDocumentId));
+
+  // Check crcs section
+  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));
+  EXPECT_THAT(crcs->component_crcs.info_crc,
+              Eq(Crc32(std::string_view(reinterpret_cast<const char*>(info),
+                                        sizeof(Info)))
+                     .Get()));
+  EXPECT_THAT(crcs->all_crc,
+              Eq(Crc32(std::string_view(
+                           reinterpret_cast<const char*>(&crcs->component_crcs),
+                           sizeof(Crcs::ComponentCrcs)))
+                     .Get()));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test,
+       InitializationShouldFailIfMissingMetadataFile) {
+  {
+    DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
+                                           /*joinable_property_id=*/20);
+    DocumentJoinIdPair child_join_id_pair2(/*document_id=*/101,
+                                           /*joinable_property_id=*/2);
+
+    // 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.
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair1,
+                   /*parent_document_ids=*/std::vector<DocumentId>{0}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair2,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1}));
+    ASSERT_THAT(index, Pointee(SizeIs(2)));
+
+    ICING_ASSERT_OK(index->PersistToDisk());
+  }
+
+  // Manually delete the metadata file.
+  const std::string metadata_file_path =
+      absl_ports::StrCat(working_path_, "/metadata");
+  ASSERT_THAT(filesystem_.DeleteFile(metadata_file_path.c_str()), IsTrue());
+
+  // Attempt to create the qualified id join index with missing metadata file.
+  // This should fail.
+  EXPECT_THAT(
+      QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                         *feature_flags_),
+      StatusIs(
+          libtextclassifier3::StatusCode::FAILED_PRECONDITION,
+          HasSubstr("Inconsistent state of qualified id join index (v3)")));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test,
+       InitializationShouldFailIfMissingParentDocumentIdToChildArrayInfoFile) {
+  {
+    DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
+                                           /*joinable_property_id=*/20);
+    DocumentJoinIdPair child_join_id_pair2(/*document_id=*/101,
+                                           /*joinable_property_id=*/2);
+
+    // 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.
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair1,
+                   /*parent_document_ids=*/std::vector<DocumentId>{0}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair2,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1}));
+    ASSERT_THAT(index, Pointee(SizeIs(2)));
+
+    ICING_ASSERT_OK(index->PersistToDisk());
+  }
+
+  // Manually delete parent_document_id_to_child_array_info file.
+  const std::string array_working_path = absl_ports::StrCat(
+      working_path_, "/parent_document_id_to_child_array_info");
+  ASSERT_THAT(filesystem_.DeleteFile(array_working_path.c_str()), IsTrue());
+
+  // Attempt to create the qualified id join index with missing
+  // parent_document_id_to_child_array_info file. This should fail.
+  EXPECT_THAT(
+      QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                         *feature_flags_),
+      StatusIs(
+          libtextclassifier3::StatusCode::FAILED_PRECONDITION,
+          HasSubstr("Inconsistent state of qualified id join index (v3)")));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test,
+       InitializationShouldFailIfMissingChildDocumentJoinIdPairArrayFile) {
+  {
+    DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
+                                           /*joinable_property_id=*/20);
+    DocumentJoinIdPair child_join_id_pair2(/*document_id=*/101,
+                                           /*joinable_property_id=*/2);
+
+    // 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.
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair1,
+                   /*parent_document_ids=*/std::vector<DocumentId>{0}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair2,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1}));
+    ASSERT_THAT(index, Pointee(SizeIs(2)));
+
+    ICING_ASSERT_OK(index->PersistToDisk());
+  }
+
+  // Manually delete child_document_join_id_pair_array file.
+  const std::string array_working_path =
+      absl_ports::StrCat(working_path_, "/child_document_join_id_pair_array");
+  ASSERT_THAT(filesystem_.DeleteFile(array_working_path.c_str()), IsTrue());
+
+  // Attempt to create the qualified id join index with missing
+  // child_document_join_id_pair_array file. This should fail.
+  EXPECT_THAT(
+      QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                         *feature_flags_),
+      StatusIs(
+          libtextclassifier3::StatusCode::FAILED_PRECONDITION,
+          HasSubstr("Inconsistent state of qualified id join index (v3)")));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test,
+       InitializationShouldFailWithoutPersistToDiskOrDestruction) {
+  DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
+                                         /*joinable_property_id=*/20);
+  DocumentJoinIdPair child_join_id_pair2(/*document_id=*/101,
+                                         /*joinable_property_id=*/2);
+
+  // 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.
+  ICING_ASSERT_OK(
+      index->Put(child_join_id_pair1,
+                 /*parent_document_ids=*/std::vector<DocumentId>{0}));
+  ICING_ASSERT_OK(
+      index->Put(child_join_id_pair2,
+                 /*parent_document_ids=*/std::vector<DocumentId>{1}));
+  ASSERT_THAT(index, Pointee(SizeIs(2)));
+
+  // GetChecksum should succeed without updating the checksum.
+  EXPECT_THAT(index->GetChecksum(), IsOk());
+
+  // Without calling PersistToDisk, checksums will not be recomputed or synced
+  // to disk, so initializing another instance on the same files should fail.
+  EXPECT_THAT(QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                                 *feature_flags_),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test,
+       InitializationShouldSucceedWithUpdateChecksums) {
+  DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
+                                         /*joinable_property_id=*/20);
+  DocumentJoinIdPair child_join_id_pair2(/*document_id=*/101,
+                                         /*joinable_property_id=*/2);
+
+  // Create new qualified id join index
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index1,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  // Insert some data.
+  ICING_ASSERT_OK(
+      index1->Put(child_join_id_pair1,
+                  /*parent_document_ids=*/std::vector<DocumentId>{0}));
+  ICING_ASSERT_OK(
+      index1->Put(child_join_id_pair2,
+                  /*parent_document_ids=*/std::vector<DocumentId>{1}));
+  ASSERT_THAT(index1, Pointee(SizeIs(2)));
+
+  // After calling UpdateChecksums, all checksums should be recomputed and
+  // synced correctly to disk, so initializing another instance on the same
+  // files should succeed, and we should be able to get the same contents.
+  ICING_ASSERT_OK_AND_ASSIGN(Crc32 crc, index1->GetChecksum());
+  EXPECT_THAT(index1->UpdateChecksums(), IsOkAndHolds(Eq(crc)));
+  EXPECT_THAT(index1->GetChecksum(), IsOkAndHolds(Eq(crc)));
+
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index2,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+  EXPECT_THAT(index2, Pointee(SizeIs(2)));
+  EXPECT_THAT(index2->Get(/*parent_document_id=*/0),
+              IsOkAndHolds(ElementsAre(child_join_id_pair1)));
+  EXPECT_THAT(index2->Get(/*parent_document_id=*/1),
+              IsOkAndHolds(ElementsAre(child_join_id_pair2)));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test,
+       InitializationShouldSucceedWithPersistToDisk) {
+  DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
+                                         /*joinable_property_id=*/20);
+  DocumentJoinIdPair child_join_id_pair2(/*document_id=*/101,
+                                         /*joinable_property_id=*/2);
+
+  // Create new qualified id join index
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index1,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  // Insert some data.
+  ICING_ASSERT_OK(
+      index1->Put(child_join_id_pair1,
+                  /*parent_document_ids=*/std::vector<DocumentId>{0}));
+  ICING_ASSERT_OK(
+      index1->Put(child_join_id_pair2,
+                  /*parent_document_ids=*/std::vector<DocumentId>{1}));
+  ASSERT_THAT(index1, Pointee(SizeIs(2)));
+
+  // After calling PersistToDisk, all checksums should be recomputed and synced
+  // correctly to disk, so initializing another instance on the same files
+  // should succeed, and we should be able to get the same contents.
+  ICING_EXPECT_OK(index1->PersistToDisk());
+
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index2,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+  EXPECT_THAT(index2, Pointee(SizeIs(2)));
+  EXPECT_THAT(index2->Get(/*parent_document_id=*/0),
+              IsOkAndHolds(ElementsAre(child_join_id_pair1)));
+  EXPECT_THAT(index2->Get(/*parent_document_id=*/1),
+              IsOkAndHolds(ElementsAre(child_join_id_pair2)));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test,
+       InitializationShouldSucceedAfterDestruction) {
+  DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
+                                         /*joinable_property_id=*/20);
+  DocumentJoinIdPair child_join_id_pair2(/*document_id=*/101,
+                                         /*joinable_property_id=*/2);
+
+  {
+    // 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.
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair1,
+                   /*parent_document_ids=*/std::vector<DocumentId>{0}));
+    ICING_ASSERT_OK(
+        index->Put(child_join_id_pair2,
+                   /*parent_document_ids=*/std::vector<DocumentId>{1}));
+    ASSERT_THAT(index, Pointee(SizeIs(2)));
+  }
+
+  {
+    // The previous instance went out of scope and was destructed. Although we
+    // didn't call PersistToDisk explicitly, the destructor should invoke it and
+    // thus initializing another instance on the same files should succeed, and
+    // we should be able to get the same contents.
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+        QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                           *feature_flags_));
+    EXPECT_THAT(index, Pointee(SizeIs(2)));
+    EXPECT_THAT(index->Get(/*parent_document_id=*/0),
+                IsOkAndHolds(ElementsAre(child_join_id_pair1)));
+    EXPECT_THAT(index->Get(/*parent_document_id=*/1),
+                IsOkAndHolds(ElementsAre(child_join_id_pair2)));
+  }
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test,
+       InitializeExistingFilesWithDifferentMagicShouldFail) {
+  {
+    // Create new qualified id join index
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+        QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                           *feature_flags_));
+    ICING_ASSERT_OK(index->Put(
+        DocumentJoinIdPair(/*document_id=*/100, /*joinable_property_id=*/20),
+        /*parent_document_ids=*/std::vector<DocumentId>{0}));
+
+    ICING_ASSERT_OK(index->PersistToDisk());
+  }
+
+  {
+    const std::string metadata_file_path =
+        absl_ports::StrCat(working_path_, "/metadata");
+    ScopedFd metadata_sfd(filesystem_.OpenForWrite(metadata_file_path.c_str()));
+    ASSERT_THAT(metadata_sfd.is_valid(), IsTrue());
+
+    auto metadata_buffer = std::make_unique<uint8_t[]>(
+        QualifiedIdJoinIndexImplV3::kMetadataFileSize);
+    ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), metadata_buffer.get(),
+                                  QualifiedIdJoinIndexImplV3::kMetadataFileSize,
+                                  /*offset=*/0),
+                IsTrue());
+
+    // Manually change magic and update checksum
+    Crcs* crcs = reinterpret_cast<Crcs*>(
+        metadata_buffer.get() +
+        QualifiedIdJoinIndexImplV3::kCrcsMetadataFileOffset);
+    Info* info = reinterpret_cast<Info*>(
+        metadata_buffer.get() +
+        QualifiedIdJoinIndexImplV3::kInfoMetadataFileOffset);
+    info->magic += 1;
+    crcs->component_crcs.info_crc = info->GetChecksum().Get();
+    crcs->all_crc = crcs->component_crcs.GetChecksum().Get();
+    ASSERT_THAT(filesystem_.PWrite(
+                    metadata_sfd.get(), /*offset=*/0, metadata_buffer.get(),
+                    QualifiedIdJoinIndexImplV3::kMetadataFileSize),
+                IsTrue());
+  }
+
+  // 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")));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test,
+       InitializeExistingFilesWithWrongAllCrcShouldFail) {
+  {
+    // Create new qualified id join index
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+        QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                           *feature_flags_));
+    ICING_ASSERT_OK(index->Put(
+        DocumentJoinIdPair(/*document_id=*/100, /*joinable_property_id=*/20),
+        /*parent_document_ids=*/std::vector<DocumentId>{0}));
+
+    ICING_ASSERT_OK(index->PersistToDisk());
+  }
+
+  {
+    const std::string metadata_file_path =
+        absl_ports::StrCat(working_path_, "/metadata");
+    ScopedFd metadata_sfd(filesystem_.OpenForWrite(metadata_file_path.c_str()));
+    ASSERT_THAT(metadata_sfd.is_valid(), IsTrue());
+
+    auto metadata_buffer = std::make_unique<uint8_t[]>(
+        QualifiedIdJoinIndexImplV3::kMetadataFileSize);
+    ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), metadata_buffer.get(),
+                                  QualifiedIdJoinIndexImplV3::kMetadataFileSize,
+                                  /*offset=*/0),
+                IsTrue());
+
+    // Manually corrupt all_crc
+    Crcs* crcs = reinterpret_cast<Crcs*>(
+        metadata_buffer.get() +
+        QualifiedIdJoinIndexImplV3::kCrcsMetadataFileOffset);
+    crcs->all_crc += 1;
+
+    ASSERT_THAT(filesystem_.PWrite(
+                    metadata_sfd.get(), /*offset=*/0, metadata_buffer.get(),
+                    QualifiedIdJoinIndexImplV3::kMetadataFileSize),
+                IsTrue());
+  }
+
+  // Attempt to create the qualified id join index with metadata containing
+  // corrupted all_crc. This should fail.
+  EXPECT_THAT(QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                                 *feature_flags_),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION,
+                       HasSubstr("Invalid all crc")));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test,
+       InitializeExistingFilesWithCorruptedInfoShouldFail) {
+  {
+    // Create new qualified id join index
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+        QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                           *feature_flags_));
+    ICING_ASSERT_OK(index->Put(
+        DocumentJoinIdPair(/*document_id=*/100, /*joinable_property_id=*/20),
+        /*parent_document_ids=*/std::vector<DocumentId>{0}));
+
+    ICING_ASSERT_OK(index->PersistToDisk());
+  }
+
+  {
+    const std::string metadata_file_path =
+        absl_ports::StrCat(working_path_, "/metadata");
+    ScopedFd metadata_sfd(filesystem_.OpenForWrite(metadata_file_path.c_str()));
+    ASSERT_THAT(metadata_sfd.is_valid(), IsTrue());
+
+    auto metadata_buffer = std::make_unique<uint8_t[]>(
+        QualifiedIdJoinIndexImplV3::kMetadataFileSize);
+    ASSERT_THAT(filesystem_.PRead(metadata_sfd.get(), metadata_buffer.get(),
+                                  QualifiedIdJoinIndexImplV3::kMetadataFileSize,
+                                  /*offset=*/0),
+                IsTrue());
+
+    // Modify info, but don't update the checksum. This would be similar to
+    // corruption of info.
+    Info* info = reinterpret_cast<Info*>(
+        metadata_buffer.get() +
+        QualifiedIdJoinIndexImplV3::kInfoMetadataFileOffset);
+    info->last_added_document_id += 1;
+
+    ASSERT_THAT(filesystem_.PWrite(
+                    metadata_sfd.get(), /*offset=*/0, metadata_buffer.get(),
+                    QualifiedIdJoinIndexImplV3::kMetadataFileSize),
+                IsTrue());
+  }
+
+  // Attempt to create the qualified id join index with info that doesn't match
+  // its checksum. This should fail.
+  EXPECT_THAT(QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                                 *feature_flags_),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION,
+                       HasSubstr("Invalid info crc")));
+}
+
+TEST_F(
+    QualifiedIdJoinIndexImplV3Test,
+    InitializeExistingFilesWithCorruptedParentDocumentIdToChildArrayInfoShouldFail) {
+  {
+    // Create new qualified id join index
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+        QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                           *feature_flags_));
+    ICING_ASSERT_OK(index->Put(
+        DocumentJoinIdPair(/*document_id=*/100, /*joinable_property_id=*/20),
+        /*parent_document_ids=*/std::vector<DocumentId>{0}));
+
+    ICING_ASSERT_OK(index->PersistToDisk());
+  }
+
+  // Corrupt parent_document_id_to_child_array_info manually.
+  {
+    const std::string array_working_path = absl_ports::StrCat(
+        working_path_, "/parent_document_id_to_child_array_info");
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<FileBackedVector<ArrayInfo>> fbv,
+        FileBackedVector<ArrayInfo>::Create(
+            filesystem_, std::move(array_working_path),
+            MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC,
+            FileBackedVector<ArrayInfo>::kMaxFileSize,
+            /*pre_mapping_mmap_size=*/0));
+    ICING_ASSERT_OK_AND_ASSIGN(Crc32 old_crc, fbv->UpdateChecksum());
+    ICING_ASSERT_OK(fbv->Append(
+        ArrayInfo(/*index_in=*/100, /*length_in=*/10, /*used_length_in=*/0)));
+    ICING_ASSERT_OK(fbv->PersistToDisk());
+    ICING_ASSERT_OK_AND_ASSIGN(Crc32 new_crc, fbv->UpdateChecksum());
+    ASSERT_THAT(old_crc, Not(Eq(new_crc)));
+  }
+
+  // Attempt to create the qualified id join index with corrupted
+  // parent_document_id_to_child_array_info. This should fail.
+  EXPECT_THAT(QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                                 *feature_flags_),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION,
+                       HasSubstr("Invalid storages crc")));
+}
+
+TEST_F(
+    QualifiedIdJoinIndexImplV3Test,
+    InitializeExistingFilesWithCorruptedChildDocumentJoinIdPairArrayShouldFail) {
+  {
+    // Create new qualified id join index
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+        QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                           *feature_flags_));
+    ICING_ASSERT_OK(index->Put(
+        DocumentJoinIdPair(/*document_id=*/100, /*joinable_property_id=*/20),
+        /*parent_document_ids=*/std::vector<DocumentId>{0}));
+
+    ICING_ASSERT_OK(index->PersistToDisk());
+  }
+
+  // Corrupt child_document_join_id_pair_array manually.
+  {
+    const std::string array_working_path =
+        absl_ports::StrCat(working_path_, "/child_document_join_id_pair_array");
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<FileBackedVector<DocumentJoinIdPair>> fbv,
+        FileBackedVector<DocumentJoinIdPair>::Create(
+            filesystem_, std::move(array_working_path),
+            MemoryMappedFile::Strategy::READ_WRITE_AUTO_SYNC,
+            FileBackedVector<DocumentJoinIdPair>::kMaxFileSize,
+            /*pre_mapping_mmap_size=*/0));
+    ICING_ASSERT_OK_AND_ASSIGN(Crc32 old_crc, fbv->UpdateChecksum());
+    ICING_ASSERT_OK(fbv->Append(DocumentJoinIdPair(/*value=*/12345)));
+    ICING_ASSERT_OK(fbv->PersistToDisk());
+    ICING_ASSERT_OK_AND_ASSIGN(Crc32 new_crc, fbv->UpdateChecksum());
+    ASSERT_THAT(old_crc, Not(Eq(new_crc)));
+  }
+
+  // Attempt to create the qualified id join index with corrupted
+  // child_document_join_id_pair_array. This should fail.
+  EXPECT_THAT(QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                                 *feature_flags_),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION,
+                       HasSubstr("Invalid storages crc")));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test, Put) {
+  // Create new qualified id join index
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  // Add 6 children with their parents to the index.
+  DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
+                                         /*joinable_property_id=*/20);
+  DocumentJoinIdPair child_join_id_pair2(/*document_id=*/101,
+                                         /*joinable_property_id=*/2);
+  DocumentJoinIdPair child_join_id_pair3(/*document_id=*/104,
+                                         /*joinable_property_id=*/4);
+  DocumentJoinIdPair child_join_id_pair4(/*document_id=*/105,
+                                         /*joinable_property_id=*/0);
+  DocumentJoinIdPair child_join_id_pair5(/*document_id=*/109,
+                                         /*joinable_property_id=*/20);
+  DocumentJoinIdPair child_join_id_pair6(/*document_id=*/121,
+                                         /*joinable_property_id=*/3);
+  EXPECT_THAT(index->Put(child_join_id_pair1,
+                         /*parent_document_ids=*/std::vector<DocumentId>{1}),
+              IsOk());
+  EXPECT_THAT(index->Put(child_join_id_pair2,
+                         /*parent_document_ids=*/std::vector<DocumentId>{1}),
+              IsOk());
+  EXPECT_THAT(index->Put(child_join_id_pair3,
+                         /*parent_document_ids=*/std::vector<DocumentId>{2}),
+              IsOk());
+  EXPECT_THAT(index->Put(child_join_id_pair4,
+                         /*parent_document_ids=*/std::vector<DocumentId>{0}),
+              IsOk());
+  EXPECT_THAT(index->Put(child_join_id_pair5,
+                         /*parent_document_ids=*/std::vector<DocumentId>{1}),
+              IsOk());
+  EXPECT_THAT(index->Put(child_join_id_pair6,
+                         /*parent_document_ids=*/std::vector<DocumentId>{5}),
+              IsOk());
+
+  EXPECT_THAT(index, Pointee(SizeIs(6)));
+
+  // Verify Get API.
+  EXPECT_THAT(index->Get(/*parent_document_id=*/0),
+              IsOkAndHolds(ElementsAre(child_join_id_pair4)));
+  EXPECT_THAT(index->Get(/*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),
+              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),
+              IsOkAndHolds(ElementsAre(child_join_id_pair6)));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test,
+       Put_multipleParentsInASingleJoinableProperty) {
+  // Create new qualified id join index
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  // Add 3 children with multiple parents to the index.
+  DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
+                                         /*joinable_property_id=*/20);
+  DocumentJoinIdPair child_join_id_pair2(/*document_id=*/101,
+                                         /*joinable_property_id=*/2);
+  DocumentJoinIdPair child_join_id_pair3(/*document_id=*/104,
+                                         /*joinable_property_id=*/4);
+  EXPECT_THAT(
+      index->Put(child_join_id_pair1,
+                 /*parent_document_ids=*/std::vector<DocumentId>{1, 4, 7, 10}),
+      IsOk());
+  EXPECT_THAT(
+      index->Put(
+          child_join_id_pair2,
+          /*parent_document_ids=*/std::vector<DocumentId>{0, 1, 2, 3, 5, 8}),
+      IsOk());
+  EXPECT_THAT(
+      index->Put(child_join_id_pair3,
+                 /*parent_document_ids=*/std::vector<DocumentId>{2, 5, 7}),
+      IsOk());
+
+  EXPECT_THAT(index, Pointee(SizeIs(13)));
+
+  // Verify Get API.
+  EXPECT_THAT(index->Get(/*parent_document_id=*/0),
+              IsOkAndHolds(ElementsAre(child_join_id_pair2)));
+  EXPECT_THAT(
+      index->Get(/*parent_document_id=*/1),
+      IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair2)));
+  EXPECT_THAT(
+      index->Get(/*parent_document_id=*/2),
+      IsOkAndHolds(ElementsAre(child_join_id_pair2, child_join_id_pair3)));
+  EXPECT_THAT(index->Get(/*parent_document_id=*/3),
+              IsOkAndHolds(ElementsAre(child_join_id_pair2)));
+  EXPECT_THAT(index->Get(/*parent_document_id=*/4),
+              IsOkAndHolds(ElementsAre(child_join_id_pair1)));
+  EXPECT_THAT(
+      index->Get(/*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->Get(/*parent_document_id=*/7),
+      IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair3)));
+  EXPECT_THAT(index->Get(/*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),
+              IsOkAndHolds(ElementsAre(child_join_id_pair1)));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test,
+       Put_multipleParentsInMultipleJoinableProperties) {
+  // Create new qualified id join index
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  // Add 1 child document with multiple parents in multiple joinable properties
+  // to the index.
+  DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
+                                         /*joinable_property_id=*/20);
+  DocumentJoinIdPair child_join_id_pair2(/*document_id=*/100,
+                                         /*joinable_property_id=*/18);
+  DocumentJoinIdPair child_join_id_pair3(/*document_id=*/100,
+                                         /*joinable_property_id=*/5);
+  EXPECT_THAT(
+      index->Put(child_join_id_pair1,
+                 /*parent_document_ids=*/std::vector<DocumentId>{1, 4, 7, 10}),
+      IsOk());
+  EXPECT_THAT(
+      index->Put(
+          child_join_id_pair2,
+          /*parent_document_ids=*/std::vector<DocumentId>{0, 1, 2, 3, 5, 8}),
+      IsOk());
+  EXPECT_THAT(
+      index->Put(child_join_id_pair3,
+                 /*parent_document_ids=*/std::vector<DocumentId>{2, 5, 7}),
+      IsOk());
+
+  EXPECT_THAT(index, Pointee(SizeIs(13)));
+
+  // Verify Get API.
+  EXPECT_THAT(index->Get(/*parent_document_id=*/0),
+              IsOkAndHolds(ElementsAre(child_join_id_pair2)));
+  EXPECT_THAT(
+      index->Get(/*parent_document_id=*/1),
+      IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair2)));
+  EXPECT_THAT(
+      index->Get(/*parent_document_id=*/2),
+      IsOkAndHolds(ElementsAre(child_join_id_pair2, child_join_id_pair3)));
+  EXPECT_THAT(index->Get(/*parent_document_id=*/3),
+              IsOkAndHolds(ElementsAre(child_join_id_pair2)));
+  EXPECT_THAT(index->Get(/*parent_document_id=*/4),
+              IsOkAndHolds(ElementsAre(child_join_id_pair1)));
+  EXPECT_THAT(
+      index->Get(/*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->Get(/*parent_document_id=*/7),
+      IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair3)));
+  EXPECT_THAT(index->Get(/*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),
+              IsOkAndHolds(ElementsAre(child_join_id_pair1)));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test,
+       PutShouldResizeParentDocumentIdToChildArrayInfo) {
+  // Create new qualified id join index
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  constexpr DocumentId kParentDocumentId = 10;
+  DocumentJoinIdPair child_join_id_pair(/*document_id=*/100,
+                                        /*joinable_property_id=*/20);
+
+  // Even though document 0 to 9 are missing in the index, adding parent
+  // document id 10 should resize the FileBackedVector and succeed without
+  // error.
+  EXPECT_THAT(
+      index->Put(
+          child_join_id_pair,
+          /*parent_document_ids=*/std::vector<DocumentId>{kParentDocumentId}),
+      IsOk());
+  EXPECT_THAT(index, Pointee(SizeIs(1)));
+
+  // 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()));
+  }
+  // Get API should return the child document for document 10.
+  EXPECT_THAT(index->Get(kParentDocumentId),
+              IsOkAndHolds(ElementsAre(child_join_id_pair)));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test,
+       PutShouldExtendChildDocumentJoinIdPairArray) {
+  // Create new qualified id join index
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  // Put 1 child for parent1.
+  DocumentId parent1 = 1;
+  DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
+                                         /*joinable_property_id=*/20);
+  ICING_ASSERT_OK(
+      index->Put(child_join_id_pair1,
+                 /*parent_document_ids=*/std::vector<DocumentId>{parent1}));
+  EXPECT_THAT(index, Pointee(SizeIs(1)));
+
+  // Put 1 child for parent2. This makes parent2's array locate right after
+  // parent1's array.
+  DocumentId parent2 = 2;
+  DocumentJoinIdPair child_join_id_pair2(/*document_id=*/101,
+                                         /*joinable_property_id=*/20);
+  ICING_ASSERT_OK(
+      index->Put(child_join_id_pair2,
+                 /*parent_document_ids=*/std::vector<DocumentId>{parent2}));
+  EXPECT_THAT(index, Pointee(SizeIs(2)));
+
+  constexpr int kNumAdditionalChildren = 100;
+  // Put 100 more children for parent1. The array storing the child document
+  // join id pairs should be extended correctly to fit all the new elements
+  // without affecting (overwriting) parent2's array.
+  std::vector<DocumentJoinIdPair> child_join_id_pairs;
+  child_join_id_pairs.reserve(kNumAdditionalChildren + 1);
+  child_join_id_pairs.push_back(child_join_id_pair1);
+  for (int i = 0; i < kNumAdditionalChildren; ++i) {
+    DocumentJoinIdPair child_join_id_pair(/*document_id=*/200 + i,
+                                          /*joinable_property_id=*/5);
+    EXPECT_THAT(
+        index->Put(child_join_id_pair,
+                   /*parent_document_ids=*/std::vector<DocumentId>{parent1}),
+        IsOk());
+    child_join_id_pairs.push_back(std::move(child_join_id_pair));
+  }
+  EXPECT_THAT(index, Pointee(SizeIs(102)));
+
+  EXPECT_THAT(index->Get(parent1), IsOkAndHolds(child_join_id_pairs));
+  EXPECT_THAT(index->Get(parent2),
+              IsOkAndHolds(ElementsAre(child_join_id_pair2)));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test, PutShouldSkipInvalidParentDocumentId) {
+  // 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_pair(/*document_id=*/100,
+                                        /*joinable_property_id=*/20);
+  EXPECT_THAT(
+      index->Put(
+          child_join_id_pair,
+          /*parent_document_ids=*/std::vector<DocumentId>{-1,
+                                                          kInvalidDocumentId, 1,
+                                                          3, 2}),
+      IsOk());
+
+  // -1, kInvalidDocumentId should be skipped, so only 3 valid join relations
+  // should be added to the index.
+  EXPECT_THAT(index, Pointee(SizeIs(3)));
+
+  // Verify Get API.
+  EXPECT_THAT(index->Get(/*parent_document_id=*/1),
+              IsOkAndHolds(ElementsAre(child_join_id_pair)));
+  EXPECT_THAT(index->Get(/*parent_document_id=*/2),
+              IsOkAndHolds(ElementsAre(child_join_id_pair)));
+  EXPECT_THAT(index->Get(/*parent_document_id=*/3),
+              IsOkAndHolds(ElementsAre(child_join_id_pair)));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test,
+       PutShouldReturnInvalidArgumentErrorForInvalidChild) {
+  // Create new qualified id join index
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  DocumentJoinIdPair invalid_child_join_id;  // Default constructor creates an
+                                             // invalid DocumentJoinIdPair.
+  ASSERT_THAT(invalid_child_join_id.is_valid(), IsFalse());
+
+  EXPECT_THAT(index->Put(invalid_child_join_id,
+                         /*parent_document_ids=*/std::vector<DocumentId>{0}),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(index, Pointee(IsEmpty()));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test, GetEmptyIndex) {
+  // 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()));
+}
+
+TEST_F(
+    QualifiedIdJoinIndexImplV3Test,
+    GetShouldReturnEmptyResultWithoutAccessingArrayForNonExistingLargeParent) {
+  // 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_pair(/*document_id=*/100,
+                                        /*joinable_property_id=*/20);
+  ICING_ASSERT_OK(index->Put(
+      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),
+              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()));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test,
+       GetShouldReturnInvalidArgumentErrorForInvalidParentDocumentId) {
+  // 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),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(index->Get(kInvalidDocumentId),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test, MigrateParent) {
+  // 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 6 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->Get(parent_doc_id1),
+      IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair2)));
+  ASSERT_THAT(index->Get(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->Get(parent_doc_id2),
+      IsOkAndHolds(ElementsAre(child_join_id_pair1, child_join_id_pair2)));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test, SetLastAddedDocumentId) {
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  EXPECT_THAT(index->last_added_document_id(), Eq(kInvalidDocumentId));
+
+  constexpr DocumentId kDocumentId = 100;
+  index->set_last_added_document_id(kDocumentId);
+  EXPECT_THAT(index->last_added_document_id(), Eq(kDocumentId));
+
+  constexpr DocumentId kNextDocumentId = 123;
+  index->set_last_added_document_id(kNextDocumentId);
+  EXPECT_THAT(index->last_added_document_id(), Eq(kNextDocumentId));
+}
+
+TEST_F(
+    QualifiedIdJoinIndexImplV3Test,
+    SetLastAddedDocumentIdShouldIgnoreNewDocumentIdNotGreaterThanTheCurrent) {
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  constexpr DocumentId kDocumentId = 123;
+  index->set_last_added_document_id(kDocumentId);
+  ASSERT_THAT(index->last_added_document_id(), Eq(kDocumentId));
+
+  constexpr DocumentId kNextDocumentId = 100;
+  ASSERT_THAT(kNextDocumentId, Lt(kDocumentId));
+  index->set_last_added_document_id(kNextDocumentId);
+  // last_added_document_id() should remain unchanged.
+  EXPECT_THAT(index->last_added_document_id(), Eq(kDocumentId));
+}
+
+TEST_F(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_));
+
+  // Create 4 parent and 7 child documents (with N to N joins):
+  // - Document 1: 101, 103, 104, 105, 107
+  // - Document 2: 102, 103, 105
+  // - Document 3: 101, 106
+  // - Document 4: 103
+  // Add 6 children with their parents to the index.
+  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);
+  DocumentJoinIdPair child_join_id_pair4(/*document_id=*/104,
+                                         /*joinable_property_id=*/0);
+  DocumentJoinIdPair child_join_id_pair5(/*document_id=*/105,
+                                         /*joinable_property_id=*/0);
+  DocumentJoinIdPair child_join_id_pair6(/*document_id=*/106,
+                                         /*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}));
+
+  ASSERT_THAT(index, Pointee(SizeIs(11)));
+  index->set_last_added_document_id(107);
+  ASSERT_THAT(index->last_added_document_id(), Eq(107));
+
+  // Delete parent 3, child 103, 107. Create a new mapping from old document id
+  // to new document id.
+  std::vector<DocumentId> document_id_old_to_new(108, kInvalidDocumentId);
+  document_id_old_to_new[1] = 0;
+  document_id_old_to_new[2] = 1;
+  document_id_old_to_new[4] = 2;
+  document_id_old_to_new[101] = 11;
+  document_id_old_to_new[102] = 12;
+  document_id_old_to_new[104] = 13;
+  document_id_old_to_new[105] = 14;
+  document_id_old_to_new[106] = 15;
+
+  // 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->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),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(/*document_id=*/11, /*joinable_property_id=*/0),
+          DocumentJoinIdPair(/*document_id=*/13, /*joinable_property_id=*/0),
+          DocumentJoinIdPair(/*document_id=*/14, /*joinable_property_id=*/0))));
+
+  // Verify document 1 (originally document 2)
+  // - Child docs 102, 105 become 12, 14.
+  // - Child doc 103 is deleted.
+  EXPECT_THAT(
+      index->Get(/*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()));
+
+  // 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()));
+
+  // Verify Put API should work normally after Optimize().
+  DocumentJoinIdPair another_child_join_id_pair(/*document_id=*/16,
+                                                /*joinable_property_id=*/0);
+  EXPECT_THAT(
+      index->Put(another_child_join_id_pair,
+                 /*parent_document_ids=*/std::vector<DocumentId>{0, 2, 3}),
+      IsOk());
+  index->set_last_added_document_id(16);
+
+  EXPECT_THAT(index, Pointee(SizeIs(8)));
+  EXPECT_THAT(index->last_added_document_id(), Eq(16));
+  EXPECT_THAT(
+      index->Get(/*parent_document_id=*/0),
+      IsOkAndHolds(ElementsAre(DocumentJoinIdPair(/*document_id=*/11,
+                                                  /*joinable_property_id=*/0),
+                               DocumentJoinIdPair(/*document_id=*/13,
+                                                  /*joinable_property_id=*/0),
+                               DocumentJoinIdPair(/*document_id=*/14,
+                                                  /*joinable_property_id=*/0),
+                               another_child_join_id_pair)));
+  EXPECT_THAT(index->Get(/*parent_document_id=*/2),
+              IsOkAndHolds(ElementsAre(another_child_join_id_pair)));
+  EXPECT_THAT(index->Get(/*parent_document_id=*/3),
+              IsOkAndHolds(ElementsAre(another_child_join_id_pair)));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test, OptimizeOutOfRangeDocumentId) {
+  // Create new qualified id join index
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  // Create 2 parent and 3 child documents (with N to N joins):
+  // - Document 1: 101, 106, 108
+  // - Document 120: 101
+  // Add 3 children with their parents to the index.
+  DocumentJoinIdPair child_join_id_pair1(/*document_id=*/101,
+                                         /*joinable_property_id=*/0);
+  DocumentJoinIdPair child_join_id_pair2(/*document_id=*/106,
+                                         /*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, 120}));
+  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}));
+
+  ASSERT_THAT(index, Pointee(SizeIs(4)));
+  index->set_last_added_document_id(120);
+  ASSERT_THAT(index->last_added_document_id(), Eq(120));
+
+  // Create document_id_old_to_new with size = 107 (from index 0 to 106), which
+  // makes parent document 120 and child document 108 out of range.
+  //
+  // Optimize should handle out of range DocumentId properly without crashing.
+  std::vector<DocumentId> document_id_old_to_new(107, kInvalidDocumentId);
+  document_id_old_to_new[1] = 0;
+  document_id_old_to_new[101] = 11;
+  document_id_old_to_new[106] = 12;
+
+  // 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=*/{},
+                      new_last_added_document_id),
+      IsOk());
+  EXPECT_THAT(index, Pointee(SizeIs(2)));
+  EXPECT_THAT(index->last_added_document_id(), Eq(new_last_added_document_id));
+
+  // Verify document 0 (originally document 1)
+  // - Child doc 101, 106 become 11, 12.
+  // - Child doc 108 is out of range, so it should be deleted.
+  EXPECT_THAT(
+      index->Get(/*parent_document_id=*/0),
+      IsOkAndHolds(ElementsAre(
+          DocumentJoinIdPair(/*document_id=*/11, /*joinable_property_id=*/0),
+          DocumentJoinIdPair(/*document_id=*/12, /*joinable_property_id=*/0))));
+}
+
+TEST_F(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_));
+
+  // Create 4 parent and 7 child documents (with N to N joins):
+  // - Document 1: 101, 103, 104, 105, 107
+  // - Document 2: 102, 103, 105
+  // - Document 3: 101, 106
+  // - Document 4: 103
+  // Add 6 children with their parents to the index.
+  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);
+  DocumentJoinIdPair child_join_id_pair4(/*document_id=*/104,
+                                         /*joinable_property_id=*/0);
+  DocumentJoinIdPair child_join_id_pair5(/*document_id=*/105,
+                                         /*joinable_property_id=*/0);
+  DocumentJoinIdPair child_join_id_pair6(/*document_id=*/106,
+                                         /*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}));
+
+  ASSERT_THAT(index, Pointee(SizeIs(11)));
+  index->set_last_added_document_id(107);
+  ASSERT_THAT(index->last_added_document_id(), Eq(107));
+
+  // Delete all documents.
+  std::vector<DocumentId> document_id_old_to_new(108, kInvalidDocumentId);
+
+  // 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, 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()));
+}
+
+TEST_F(QualifiedIdJoinIndexImplV3Test, Clear) {
+  // Create new qualified id join index
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<QualifiedIdJoinIndexImplV3> index,
+                             QualifiedIdJoinIndexImplV3::Create(
+                                 filesystem_, working_path_, *feature_flags_));
+
+  // Add 6 children with their parents to the index.
+  DocumentJoinIdPair child_join_id_pair1(/*document_id=*/100,
+                                         /*joinable_property_id=*/20);
+  DocumentJoinIdPair child_join_id_pair2(/*document_id=*/101,
+                                         /*joinable_property_id=*/2);
+  DocumentJoinIdPair child_join_id_pair3(/*document_id=*/104,
+                                         /*joinable_property_id=*/4);
+  DocumentJoinIdPair child_join_id_pair4(/*document_id=*/105,
+                                         /*joinable_property_id=*/0);
+  ICING_ASSERT_OK(
+      index->Put(child_join_id_pair1,
+                 /*parent_document_ids=*/std::vector<DocumentId>{1}));
+  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>{2}));
+  ICING_ASSERT_OK(
+      index->Put(child_join_id_pair4,
+                 /*parent_document_ids=*/std::vector<DocumentId>{0}));
+
+  ASSERT_THAT(index, Pointee(SizeIs(4)));
+  index->set_last_added_document_id(105);
+  ASSERT_THAT(index->last_added_document_id(), Eq(105));
+
+  // After Clear(), last_added_document_id should be set to kInvalidDocumentId,
+  // and the previous added data should be deleted.
+  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()));
+
+  // Join index should be able to work normally after Clear().
+  EXPECT_THAT(index->Put(child_join_id_pair4,
+                         /*parent_document_ids=*/std::vector<DocumentId>{5}),
+              IsOk());
+  index->set_last_added_document_id(105);
+
+  EXPECT_THAT(index, Pointee(SizeIs(1)));
+  EXPECT_THAT(index->last_added_document_id(), Eq(105));
+  EXPECT_THAT(index->Get(/*parent_document_id=*/5),
+              IsOkAndHolds(ElementsAre(child_join_id_pair4)));
+
+  ICING_ASSERT_OK(index->PersistToDisk());
+  index.reset();
+
+  // Verify index after reconstructing.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      index, QualifiedIdJoinIndexImplV3::Create(filesystem_, working_path_,
+                                                *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),
+              IsOkAndHolds(ElementsAre(child_join_id_pair4)));
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/join/qualified-id-join-index.h b/icing/join/qualified-id-join-index.h
index 9b40c4f..4972c5e 100644
--- a/icing/join/qualified-id-join-index.h
+++ b/icing/join/qualified-id-join-index.h
@@ -26,12 +26,12 @@
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/persistent-storage.h"
-#include "icing/join/doc-join-info.h"
 #include "icing/join/document-id-to-join-info.h"
+#include "icing/join/document-join-id-pair.h"
 #include "icing/schema/joinable-property.h"
 #include "icing/store/document-filter-data.h"
 #include "icing/store/document-id.h"
-#include "icing/store/namespace-fingerprint-identifier.h"
+#include "icing/store/namespace-id-fingerprint.h"
 #include "icing/store/namespace-id.h"
 #include "icing/util/crc32.h"
 
@@ -48,10 +48,12 @@
 
     virtual libtextclassifier3::Status Advance() = 0;
 
-    virtual const DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>&
-    GetCurrent() const = 0;
+    virtual const DocumentIdToJoinInfo<NamespaceIdFingerprint>& GetCurrent()
+        const = 0;
   };
 
+  enum class Version { kV1, kV2, kV3 };
+
   static constexpr WorkingPathType kWorkingPathType =
       WorkingPathType::kDirectory;
 
@@ -68,7 +70,7 @@
 
   virtual ~QualifiedIdJoinIndex() override = default;
 
-  // (v1 only) Puts a new data into index: DocJoinInfo (DocumentId,
+  // (v1 only) Puts a new data into index: DocumentJoinIdPair (DocumentId,
   // JoinablePropertyId) references to ref_qualified_id_str (the identifier of
   // another document).
   //
@@ -79,11 +81,11 @@
   //   - INVALID_ARGUMENT_ERROR if doc_join_info is invalid
   //   - Any KeyMapper errors
   virtual libtextclassifier3::Status Put(
-      const DocJoinInfo& doc_join_info,
+      const DocumentJoinIdPair& document_join_id_pair,
       std::string_view ref_qualified_id_str) = 0;
 
-  // (v2 only) Puts a list of referenced NamespaceFingerprintIdentifier into
-  // index, given the DocumentId, SchemaTypeId and JoinablePropertyId.
+  // (v2 only) Puts a list of referenced NamespaceIdFingerprint into index,
+  // given the DocumentId, SchemaTypeId and JoinablePropertyId.
   //
   // Returns:
   //   - OK on success
@@ -93,20 +95,31 @@
   virtual libtextclassifier3::Status Put(
       SchemaTypeId schema_type_id, JoinablePropertyId joinable_property_id,
       DocumentId document_id,
-      std::vector<NamespaceFingerprintIdentifier>&&
-          ref_namespace_fingerprint_ids) = 0;
+      std::vector<NamespaceIdFingerprint>&&
+          ref_namespace_id_uri_fingerprints) = 0;
 
-  // (v1 only) Gets the referenced document's qualified id string by
-  // DocJoinInfo.
+  // (v3 only) Puts a new child document and its referenced parent documents
+  // into the join index.
   //
   // Returns:
-  //   - A qualified id string referenced by the given DocJoinInfo (DocumentId,
-  //     JoinablePropertyId) on success
+  //   - OK on success
+  //   - INVALID_ARGUMENT_ERROR if child_document_join_id_pair is invalid
+  //   - Any FileBackedVector errors
+  virtual libtextclassifier3::Status Put(
+      const DocumentJoinIdPair& child_document_join_id_pair,
+      std::vector<DocumentId>&& parent_document_ids) = 0;
+
+  // (v1 only) Gets the referenced document's qualified id string by
+  // DocumentJoinIdPair.
+  //
+  // Returns:
+  //   - A qualified id string referenced by the given DocumentJoinIdPair
+  //     (DocumentId, JoinablePropertyId) on success
   //   - INVALID_ARGUMENT_ERROR if doc_join_info is invalid
   //   - NOT_FOUND_ERROR if doc_join_info doesn't exist
   //   - Any KeyMapper errors
   virtual libtextclassifier3::StatusOr<std::string_view> Get(
-      const DocJoinInfo& doc_join_info) const = 0;
+      const DocumentJoinIdPair& document_join_id_pair) const = 0;
 
   // (v2 only) Returns a JoinDataIterator for iterating through all join data of
   // the specified (schema_type_id, joinable_property_id).
@@ -120,6 +133,25 @@
   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.
+  //
+  // Returns:
+  //   - A list of children's DocumentJoinIdPair on success
+  //   - Any FileBackedVector errors
+  virtual libtextclassifier3::StatusOr<std::vector<DocumentJoinIdPair>> Get(
+      DocumentId parent_document_id) const = 0;
+
+  // Migrates existing join data for a parent document from old_document_id to
+  // new_document_id if necessary.
+  //
+  // Returns:
+  //   - OK on success
+  //   - INVALID_ARGUMENT_ERROR if any document id is invalid
+  //   - Any errors, depending on the implementation
+  virtual libtextclassifier3::Status MigrateParent(
+      DocumentId old_document_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.
@@ -149,7 +181,7 @@
   //   - INTERNAL_ERROR on I/O error
   virtual libtextclassifier3::Status Clear() = 0;
 
-  virtual bool is_v2() const = 0;
+  virtual Version version() const = 0;
 
   virtual int32_t size() const = 0;
 
diff --git a/icing/join/qualified-id-join-indexing-handler-v1_test.cc b/icing/join/qualified-id-join-indexing-handler-v1_test.cc
index c8fdab4..e1e3881 100644
--- a/icing/join/qualified-id-join-indexing-handler-v1_test.cc
+++ b/icing/join/qualified-id-join-indexing-handler-v1_test.cc
@@ -21,8 +21,10 @@
 #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-v1.h"
 #include "icing/join/qualified-id-join-index.h"
 #include "icing/join/qualified-id-join-indexing-handler.h"
@@ -37,11 +39,12 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.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/tokenized-document.h"
 #include "unicode/uloc.h"
 
@@ -81,10 +84,11 @@
 class QualifiedIdJoinIndexingHandlerV1Test : public ::testing::Test {
  protected:
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
 
@@ -111,8 +115,8 @@
         filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str()),
         IsTrue());
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     SchemaProto schema =
         SchemaBuilder()
             .AddType(
@@ -152,13 +156,12 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
         DocumentStore::Create(&filesystem_, doc_store_dir_, &fake_clock_,
-                              schema_store_.get(),
+                              schema_store_.get(), feature_flags_.get(),
                               /*force_recovery_and_revalidate_documents=*/false,
-                              /*namespace_id_fingerprint=*/true,
                               /*pre_mapping_fbv=*/false,
                               /*use_persistent_hash_map=*/true,
                               PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDeflateCompressionLevel,
+                                  DocumentWrapper>::kDefaultCompressionLevel,
                               /*initialize_stats=*/nullptr));
     doc_store_ = std::move(create_result.document_store);
   }
@@ -172,6 +175,7 @@
     filesystem_.DeleteDirectoryRecursively(base_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   FakeClock fake_clock_;
   std::string base_dir_;
@@ -232,12 +236,13 @@
                                              qualified_id_join_index_.get()));
   EXPECT_THAT(
       handler->Handle(tokenized_document, kDefaultDocumentId,
+                      /*old_document_id=*/kInvalidDocumentId,
                       /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
       IsOk());
 
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(kDefaultDocumentId));
-  EXPECT_THAT(qualified_id_join_index_->Get(DocJoinInfo(
+  EXPECT_THAT(qualified_id_join_index_->Get(DocumentJoinIdPair(
                   kDefaultDocumentId, kQualifiedIdJoinablePropertyId)),
               IsOkAndHolds("pkg$db/ns#ref_type/1"));
 }
@@ -283,17 +288,18 @@
       std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
                                              qualified_id_join_index_.get()));
-  EXPECT_THAT(handler->Handle(tokenized_document, kDefaultDocumentId,
-                              /*recovery_mode=*/false,
-                              /*put_document_stats=*/nullptr),
-              IsOk());
+  EXPECT_THAT(
+      handler->Handle(tokenized_document, kDefaultDocumentId,
+                      /*old_document_id=*/kInvalidDocumentId,
+                      /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
+      IsOk());
 
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(kDefaultDocumentId));
-  EXPECT_THAT(qualified_id_join_index_->Get(DocJoinInfo(
+  EXPECT_THAT(qualified_id_join_index_->Get(DocumentJoinIdPair(
                   kDefaultDocumentId, kNestedQualifiedIdJoinablePropertyId)),
               IsOkAndHolds("pkg$db/ns#ref_type/2"));
-  EXPECT_THAT(qualified_id_join_index_->Get(DocJoinInfo(
+  EXPECT_THAT(qualified_id_join_index_->Get(DocumentJoinIdPair(
                   kDefaultDocumentId, kQualifiedId2JoinablePropertyId)),
               IsOkAndHolds("pkg$db/ns#ref_type/1"));
 }
@@ -328,11 +334,12 @@
                                              qualified_id_join_index_.get()));
   EXPECT_THAT(
       handler->Handle(tokenized_document, kDefaultDocumentId,
+                      /*old_document_id=*/kInvalidDocumentId,
                       /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
       IsOk());
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(kDefaultDocumentId));
-  EXPECT_THAT(qualified_id_join_index_->Get(DocJoinInfo(
+  EXPECT_THAT(qualified_id_join_index_->Get(DocumentJoinIdPair(
                   kDefaultDocumentId, kQualifiedIdJoinablePropertyId)),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 }
@@ -359,11 +366,12 @@
                                              qualified_id_join_index_.get()));
   EXPECT_THAT(
       handler->Handle(tokenized_document, kDefaultDocumentId,
+                      /*old_document_id=*/kInvalidDocumentId,
                       /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
       IsOk());
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(kDefaultDocumentId));
-  EXPECT_THAT(qualified_id_join_index_->Get(DocJoinInfo(
+  EXPECT_THAT(qualified_id_join_index_->Get(DocumentJoinIdPair(
                   kDefaultDocumentId, kQualifiedIdJoinablePropertyId)),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 }
@@ -402,22 +410,24 @@
   // index data and last_added_document_id should remain unchanged.
   EXPECT_THAT(
       handler->Handle(tokenized_document, kInvalidDocumentId,
+                      /*old_document_id=*/kInvalidDocumentId,
                       /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(kDefaultDocumentId));
-  EXPECT_THAT(qualified_id_join_index_->Get(DocJoinInfo(
+  EXPECT_THAT(qualified_id_join_index_->Get(DocumentJoinIdPair(
                   kInvalidDocumentId, kQualifiedIdJoinablePropertyId)),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
   // Recovery mode should get the same result.
   EXPECT_THAT(
       handler->Handle(tokenized_document, kInvalidDocumentId,
-                      /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
+                      /*old_document_id=*/kInvalidDocumentId,
+                      /*recovery_mode=*/true, /*put_document_stats=*/nullptr),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(kDefaultDocumentId));
-  EXPECT_THAT(qualified_id_join_index_->Get(DocJoinInfo(
+  EXPECT_THAT(qualified_id_join_index_->Get(DocumentJoinIdPair(
                   kInvalidDocumentId, kQualifiedIdJoinablePropertyId)),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 }
@@ -458,11 +468,12 @@
   ASSERT_THAT(IsDocumentIdValid(kDefaultDocumentId - 1), IsTrue());
   EXPECT_THAT(
       handler->Handle(tokenized_document, kDefaultDocumentId - 1,
+                      /*old_document_id=*/kInvalidDocumentId,
                       /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(kDefaultDocumentId));
-  EXPECT_THAT(qualified_id_join_index_->Get(DocJoinInfo(
+  EXPECT_THAT(qualified_id_join_index_->Get(DocumentJoinIdPair(
                   kDefaultDocumentId, kQualifiedIdJoinablePropertyId)),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
@@ -471,11 +482,12 @@
   // unchanged.
   EXPECT_THAT(
       handler->Handle(tokenized_document, kDefaultDocumentId,
+                      /*old_document_id=*/kInvalidDocumentId,
                       /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(kDefaultDocumentId));
-  EXPECT_THAT(qualified_id_join_index_->Get(DocJoinInfo(
+  EXPECT_THAT(qualified_id_join_index_->Get(DocumentJoinIdPair(
                   kDefaultDocumentId, kQualifiedIdJoinablePropertyId)),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 }
@@ -516,11 +528,12 @@
   ASSERT_THAT(IsDocumentIdValid(kDefaultDocumentId - 1), IsTrue());
   EXPECT_THAT(
       handler->Handle(tokenized_document, kDefaultDocumentId - 1,
+                      /*old_document_id=*/kInvalidDocumentId,
                       /*recovery_mode=*/true, /*put_document_stats=*/nullptr),
       IsOk());
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(kDefaultDocumentId));
-  EXPECT_THAT(qualified_id_join_index_->Get(DocJoinInfo(
+  EXPECT_THAT(qualified_id_join_index_->Get(DocumentJoinIdPair(
                   kDefaultDocumentId, kQualifiedIdJoinablePropertyId)),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
@@ -530,11 +543,12 @@
   // unchanged.
   EXPECT_THAT(
       handler->Handle(tokenized_document, kDefaultDocumentId,
+                      /*old_document_id=*/kInvalidDocumentId,
                       /*recovery_mode=*/true, /*put_document_stats=*/nullptr),
       IsOk());
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(kDefaultDocumentId));
-  EXPECT_THAT(qualified_id_join_index_->Get(DocJoinInfo(
+  EXPECT_THAT(qualified_id_join_index_->Get(DocumentJoinIdPair(
                   kDefaultDocumentId, kQualifiedIdJoinablePropertyId)),
               StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
 
@@ -543,11 +557,12 @@
   ASSERT_THAT(IsDocumentIdValid(kDefaultDocumentId + 1), IsTrue());
   EXPECT_THAT(
       handler->Handle(tokenized_document, kDefaultDocumentId + 1,
+                      /*old_document_id=*/kInvalidDocumentId,
                       /*recovery_mode=*/true, /*put_document_stats=*/nullptr),
       IsOk());
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(kDefaultDocumentId + 1));
-  EXPECT_THAT(qualified_id_join_index_->Get(DocJoinInfo(
+  EXPECT_THAT(qualified_id_join_index_->Get(DocumentJoinIdPair(
                   kDefaultDocumentId + 1, kQualifiedIdJoinablePropertyId)),
               IsOkAndHolds("pkg$db/ns#ref_type/1"));
 }
diff --git a/icing/join/qualified-id-join-indexing-handler_test.cc b/icing/join/qualified-id-join-indexing-handler-v2_test.cc
similarity index 87%
rename from icing/join/qualified-id-join-indexing-handler_test.cc
rename to icing/join/qualified-id-join-indexing-handler-v2_test.cc
index 95c0327..9c20d45 100644
--- a/icing/join/qualified-id-join-indexing-handler_test.cc
+++ b/icing/join/qualified-id-join-indexing-handler-v2_test.cc
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "icing/join/qualified-id-join-indexing-handler.h"
-
 #include <memory>
 #include <string>
 #include <string_view>
@@ -26,11 +24,13 @@
 #include "gtest/gtest.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/portable-file-backed-proto-log.h"
 #include "icing/join/document-id-to-join-info.h"
 #include "icing/join/qualified-id-join-index-impl-v2.h"
 #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/platform.h"
 #include "icing/proto/document.pb.h"
@@ -41,15 +41,16 @@
 #include "icing/store/document-filter-data.h"
 #include "icing/store/document-id.h"
 #include "icing/store/document-store.h"
-#include "icing/store/namespace-fingerprint-identifier.h"
+#include "icing/store/namespace-id-fingerprint.h"
 #include "icing/store/namespace-id.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.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"
@@ -80,13 +81,14 @@
 static constexpr std::string_view kPropertyNestedDoc = "nested";
 static constexpr std::string_view kPropertyQualifiedId2 = "qualifiedId2";
 
-class QualifiedIdJoinIndexingHandlerTest : public ::testing::Test {
+class QualifiedIdJoinIndexingHandlerV2Test : public ::testing::Test {
  protected:
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
 
@@ -112,8 +114,8 @@
         filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str()),
         IsTrue());
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     SchemaProto schema =
         SchemaBuilder()
             .AddType(
@@ -153,13 +155,12 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
         DocumentStore::Create(&filesystem_, doc_store_dir_, &fake_clock_,
-                              schema_store_.get(),
+                              schema_store_.get(), feature_flags_.get(),
                               /*force_recovery_and_revalidate_documents=*/false,
-                              /*namespace_id_fingerprint=*/true,
                               /*pre_mapping_fbv=*/false,
                               /*use_persistent_hash_map=*/true,
                               PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDeflateCompressionLevel,
+                                  DocumentWrapper>::kDefaultCompressionLevel,
                               /*initialize_stats=*/nullptr));
     doc_store_ = std::move(create_result.document_store);
 
@@ -200,6 +201,7 @@
     filesystem_.DeleteDirectoryRecursively(base_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   FakeClock fake_clock_;
   std::string base_dir_;
@@ -239,7 +241,8 @@
   return result;
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerTest, CreationWithNullPointerShouldFail) {
+TEST_F(QualifiedIdJoinIndexingHandlerV2Test,
+       CreationWithNullPointerShouldFail) {
   EXPECT_THAT(
       QualifiedIdJoinIndexingHandler::Create(
           /*clock=*/nullptr, doc_store_.get(), qualified_id_join_index_.get()),
@@ -256,7 +259,7 @@
       StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerTest, HandleJoinableProperty) {
+TEST_F(QualifiedIdJoinIndexingHandlerV2Test, HandleJoinableProperty) {
   // Create and put referenced (parent) document. Get its document id and
   // namespace id.
   DocumentProto referenced_document =
@@ -271,9 +274,9 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       NamespaceId ref_doc_ns_id,
       doc_store_->GetNamespaceId(referenced_document.namespace_()));
-  NamespaceFingerprintIdentifier ref_doc_ns_fingerprint_id(
+  NamespaceIdFingerprint ref_doc_nsid_uri_fingerprint(
       /*namespace_id=*/ref_doc_ns_id, /*target_str=*/referenced_document.uri());
-  ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_ns_fingerprint_id),
+  ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_nsid_uri_fingerprint),
               IsOkAndHolds(ref_doc_id));
 
   // Create and put (child) document. Also tokenize it.
@@ -299,24 +302,23 @@
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
                                              qualified_id_join_index_.get()));
   EXPECT_THAT(
-      handler->Handle(tokenized_document, doc_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
+      handler->Handle(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));
   // (kFakeType, kPropertyQualifiedId) should contain
-  // [(doc_id, ref_doc_ns_fingerprint_id)].
+  // [(doc_id, ref_doc_nsid_uri_fingerprint)].
   EXPECT_THAT(
       GetJoinData(*qualified_id_join_index_, /*schema_type_id=*/fake_type_id_,
                   /*joinable_property_id=*/fake_type_joinable_property_id_),
-      IsOkAndHolds(
-          ElementsAre(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/doc_id,
-              /*join_info=*/ref_doc_ns_fingerprint_id))));
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/doc_id,
+          /*join_info=*/ref_doc_nsid_uri_fingerprint))));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerTest, HandleNestedJoinableProperty) {
+TEST_F(QualifiedIdJoinIndexingHandlerV2Test, HandleNestedJoinableProperty) {
   // Create and put referenced (parent) document1. Get its document id and
   // namespace id.
   DocumentProto referenced_document1 =
@@ -331,10 +333,10 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       NamespaceId ref_doc_ns_id1,
       doc_store_->GetNamespaceId(referenced_document1.namespace_()));
-  NamespaceFingerprintIdentifier ref_doc_ns_fingerprint_id1(
+  NamespaceIdFingerprint ref_doc_nsid_uri_fingerprint1(
       /*namespace_id=*/ref_doc_ns_id1,
       /*target_str=*/referenced_document1.uri());
-  ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_ns_fingerprint_id1),
+  ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_nsid_uri_fingerprint1),
               IsOkAndHolds(ref_doc_id1));
 
   // Create and put referenced (parent) document2. Get its document id and
@@ -351,10 +353,10 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       NamespaceId ref_doc_ns_id2,
       doc_store_->GetNamespaceId(referenced_document2.namespace_()));
-  NamespaceFingerprintIdentifier ref_doc_ns_fingerprint_id2(
+  NamespaceIdFingerprint ref_doc_nsid_uri_fingerprint2(
       /*namespace_id=*/ref_doc_ns_id2,
       /*target_str=*/referenced_document2.uri());
-  ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_ns_fingerprint_id2),
+  ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_nsid_uri_fingerprint2),
               IsOkAndHolds(ref_doc_id2));
 
   // Create and put (child) document:
@@ -393,8 +395,8 @@
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
                                              qualified_id_join_index_.get()));
   EXPECT_THAT(
-      handler->Handle(tokenized_document, doc_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
+      handler->Handle(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().
@@ -405,27 +407,25 @@
                   /*joinable_property_id=*/fake_type_joinable_property_id_),
       IsOkAndHolds(IsEmpty()));
   // (kNestedType, kPropertyNestedDoc.kPropertyQualifiedId) should contain
-  // [(doc_id, ref_doc_ns_fingerprint_id2)].
+  // [(doc_id, ref_doc_nsid_uri_fingerprint2)].
   EXPECT_THAT(
       GetJoinData(
           *qualified_id_join_index_, /*schema_type_id=*/nested_type_id_,
           /*joinable_property_id=*/nested_type_nested_joinable_property_id_),
-      IsOkAndHolds(
-          ElementsAre(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/doc_id,
-              /*join_info=*/ref_doc_ns_fingerprint_id2))));
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/doc_id,
+          /*join_info=*/ref_doc_nsid_uri_fingerprint2))));
   // (kNestedType, kPropertyQualifiedId2) should contain
-  // [(doc_id, ref_doc_ns_fingerprint_id1)].
+  // [(doc_id, ref_doc_nsid_uri_fingerprint1)].
   EXPECT_THAT(
       GetJoinData(*qualified_id_join_index_, /*schema_type_id=*/nested_type_id_,
                   /*joinable_property_id=*/nested_type_joinable_property_id_),
-      IsOkAndHolds(
-          ElementsAre(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/doc_id,
-              /*join_info=*/ref_doc_ns_fingerprint_id1))));
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/doc_id,
+          /*join_info=*/ref_doc_nsid_uri_fingerprint1))));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerTest,
+TEST_F(QualifiedIdJoinIndexingHandlerV2Test,
        HandleShouldSkipInvalidFormatQualifiedId) {
   static constexpr std::string_view kInvalidFormatQualifiedId =
       "invalid_format_qualified_id";
@@ -457,8 +457,8 @@
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
                                              qualified_id_join_index_.get()));
   EXPECT_THAT(
-      handler->Handle(tokenized_document, doc_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
+      handler->Handle(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(). Index data
@@ -472,7 +472,7 @@
       IsOkAndHolds(IsEmpty()));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerTest,
+TEST_F(QualifiedIdJoinIndexingHandlerV2Test,
        HandleShouldSkipNonExistingNamespace) {
   static constexpr std::string_view kUnknownNamespace = "UnknownNamespace";
   // Create and put (child) document which references to a parent qualified id
@@ -501,8 +501,8 @@
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
                                              qualified_id_join_index_.get()));
   EXPECT_THAT(
-      handler->Handle(tokenized_document, doc_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
+      handler->Handle(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().
@@ -515,7 +515,7 @@
       IsOkAndHolds(IsEmpty()));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerTest, HandleShouldSkipEmptyQualifiedId) {
+TEST_F(QualifiedIdJoinIndexingHandlerV2Test, HandleShouldSkipEmptyQualifiedId) {
   // Create and put (child) document without any qualified id. Also tokenize it.
   DocumentProto document = DocumentBuilder()
                                .SetKey("icing", "fake_type/1")
@@ -538,8 +538,8 @@
       QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
                                              qualified_id_join_index_.get()));
   EXPECT_THAT(
-      handler->Handle(tokenized_document, doc_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
+      handler->Handle(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(). Index data
@@ -553,7 +553,7 @@
       IsOkAndHolds(IsEmpty()));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerTest,
+TEST_F(QualifiedIdJoinIndexingHandlerV2Test,
        HandleInvalidDocumentIdShouldReturnInvalidArgumentError) {
   // Create and put referenced (parent) document. Get its document id and
   // namespace id.
@@ -569,9 +569,9 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       NamespaceId ref_doc_ns_id,
       doc_store_->GetNamespaceId(referenced_document.namespace_()));
-  NamespaceFingerprintIdentifier ref_doc_ns_fingerprint_id(
+  NamespaceIdFingerprint ref_doc_nsid_uri_fingerprint(
       /*namespace_id=*/ref_doc_ns_id, /*target_str=*/referenced_document.uri());
-  ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_ns_fingerprint_id),
+  ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_nsid_uri_fingerprint),
               IsOkAndHolds(ref_doc_id));
 
   // Create and put (child) document. Also tokenize it.
@@ -600,6 +600,7 @@
   // Handling document with kInvalidDocumentId should cause a failure.
   EXPECT_THAT(
       handler->Handle(tokenized_document, kInvalidDocumentId,
+                      /*old_document_id=*/kInvalidDocumentId,
                       /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   // Verify the state of qualified_id_join_index_ after Handle(). Both index
@@ -615,7 +616,8 @@
   // Recovery mode should get the same result.
   EXPECT_THAT(
       handler->Handle(tokenized_document, kInvalidDocumentId,
-                      /*recovery_mode=*/false, /*put_document_stats=*/nullptr),
+                      /*old_document_id=*/kInvalidDocumentId,
+                      /*recovery_mode=*/true, /*put_document_stats=*/nullptr),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(ref_doc_id));
@@ -626,7 +628,7 @@
       IsOkAndHolds(IsEmpty()));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerTest,
+TEST_F(QualifiedIdJoinIndexingHandlerV2Test,
        HandleOutOfOrderDocumentIdShouldReturnInvalidArgumentError) {
   // Create and put referenced (parent) document. Get its document id and
   // namespace id.
@@ -642,9 +644,9 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       NamespaceId ref_doc_ns_id,
       doc_store_->GetNamespaceId(referenced_document.namespace_()));
-  NamespaceFingerprintIdentifier ref_doc_ns_fingerprint_id(
+  NamespaceIdFingerprint ref_doc_nsid_uri_fingerprint(
       /*namespace_id=*/ref_doc_ns_id, /*target_str=*/referenced_document.uri());
-  ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_ns_fingerprint_id),
+  ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_nsid_uri_fingerprint),
               IsOkAndHolds(ref_doc_id));
 
   // Create and put (child) document. Also tokenize it.
@@ -672,8 +674,8 @@
   qualified_id_join_index_->set_last_added_document_id(doc_id);
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(), Eq(doc_id));
   EXPECT_THAT(
-      handler->Handle(tokenized_document, doc_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
+      handler->Handle(tokenized_document, doc_id, 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.
@@ -690,8 +692,8 @@
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(doc_id + 1));
   EXPECT_THAT(
-      handler->Handle(tokenized_document, doc_id, /*recovery_mode=*/false,
-                      /*put_document_stats=*/nullptr),
+      handler->Handle(tokenized_document, doc_id, 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.
@@ -704,7 +706,7 @@
       IsOkAndHolds(IsEmpty()));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerTest,
+TEST_F(QualifiedIdJoinIndexingHandlerV2Test,
        HandleRecoveryModeShouldIndexDocsGtLastAddedDocId) {
   // Create and put referenced (parent) document. Get its document id and
   // namespace id.
@@ -720,9 +722,9 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       NamespaceId ref_doc_ns_id,
       doc_store_->GetNamespaceId(referenced_document.namespace_()));
-  NamespaceFingerprintIdentifier ref_doc_ns_fingerprint_id(
+  NamespaceIdFingerprint ref_doc_nsid_uri_fingerprint(
       /*namespace_id=*/ref_doc_ns_id, /*target_str=*/referenced_document.uri());
-  ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_ns_fingerprint_id),
+  ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_nsid_uri_fingerprint),
               IsOkAndHolds(ref_doc_id));
 
   // Create and put (child) document. Also tokenize it.
@@ -751,20 +753,19 @@
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(doc_id - 1));
   EXPECT_THAT(
-      handler->Handle(tokenized_document, doc_id, /*recovery_mode=*/true,
-                      /*put_document_stats=*/nullptr),
+      handler->Handle(tokenized_document, doc_id, put_result.old_document_id,
+                      /*recovery_mode=*/true, /*put_document_stats=*/nullptr),
       IsOk());
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(), Eq(doc_id));
   EXPECT_THAT(
       GetJoinData(*qualified_id_join_index_, /*schema_type_id=*/fake_type_id_,
                   /*joinable_property_id=*/fake_type_joinable_property_id_),
-      IsOkAndHolds(
-          ElementsAre(DocumentIdToJoinInfo<NamespaceFingerprintIdentifier>(
-              /*document_id=*/doc_id,
-              /*join_info=*/ref_doc_ns_fingerprint_id))));
+      IsOkAndHolds(ElementsAre(DocumentIdToJoinInfo<NamespaceIdFingerprint>(
+          /*document_id=*/doc_id,
+          /*join_info=*/ref_doc_nsid_uri_fingerprint))));
 }
 
-TEST_F(QualifiedIdJoinIndexingHandlerTest,
+TEST_F(QualifiedIdJoinIndexingHandlerV2Test,
        HandleRecoveryModeShouldIgnoreDocsLeLastAddedDocId) {
   // Create and put referenced (parent) document. Get its document id and
   // namespace id.
@@ -780,9 +781,9 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       NamespaceId ref_doc_ns_id,
       doc_store_->GetNamespaceId(referenced_document.namespace_()));
-  NamespaceFingerprintIdentifier ref_doc_ns_fingerprint_id(
+  NamespaceIdFingerprint ref_doc_nsid_uri_fingerprint(
       /*namespace_id=*/ref_doc_ns_id, /*target_str=*/referenced_document.uri());
-  ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_ns_fingerprint_id),
+  ASSERT_THAT(doc_store_->GetDocumentId(ref_doc_nsid_uri_fingerprint),
               IsOkAndHolds(ref_doc_id));
 
   // Create and put (child) document. Also tokenize it.
@@ -812,8 +813,8 @@
   qualified_id_join_index_->set_last_added_document_id(doc_id);
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(), Eq(doc_id));
   EXPECT_THAT(
-      handler->Handle(tokenized_document, doc_id, /*recovery_mode=*/true,
-                      /*put_document_stats=*/nullptr),
+      handler->Handle(tokenized_document, doc_id, put_result.old_document_id,
+                      /*recovery_mode=*/true, /*put_document_stats=*/nullptr),
       IsOk());
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(), Eq(doc_id));
   // (kFakeType, kPropertyQualifiedId) should contain nothing.
@@ -829,8 +830,8 @@
   ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(doc_id + 1));
   EXPECT_THAT(
-      handler->Handle(tokenized_document, doc_id, /*recovery_mode=*/true,
-                      /*put_document_stats=*/nullptr),
+      handler->Handle(tokenized_document, doc_id, put_result.old_document_id,
+                      /*recovery_mode=*/true, /*put_document_stats=*/nullptr),
       IsOk());
   EXPECT_THAT(qualified_id_join_index_->last_added_document_id(),
               Eq(doc_id + 1));
diff --git a/icing/join/qualified-id-join-indexing-handler-v3_test.cc b/icing/join/qualified-id-join-indexing-handler-v3_test.cc
new file mode 100644
index 0000000..5c87e19
--- /dev/null
+++ b/icing/join/qualified-id-join-indexing-handler-v3_test.cc
@@ -0,0 +1,808 @@
+// Copyright (C) 2024 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 <string_view>
+#include <utility>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#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/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-v3.h"
+#include "icing/join/qualified-id-join-indexing-handler.h"
+#include "icing/join/qualified-id.h"
+#include "icing/portable/platform.h"
+#include "icing/proto/document.pb.h"
+#include "icing/proto/schema.pb.h"
+#include "icing/schema-builder.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/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/tokenized-document.h"
+#include "unicode/uloc.h"
+
+namespace icing {
+namespace lib {
+
+namespace {
+
+using ::testing::ElementsAre;
+using ::testing::Eq;
+using ::testing::IsEmpty;
+using ::testing::IsTrue;
+using ::testing::NotNull;
+using ::testing::Pointee;
+using ::testing::SizeIs;
+
+// Schema type for referenced documents: ReferencedType
+static constexpr std::string_view kReferencedType = "ReferencedType";
+static constexpr std::string_view kPropertyName = "name";
+
+// Joinable properties and joinable property id. Joinable property id is
+// determined by the lexicographical order of joinable property path.
+// Schema type with joinable property: FakeType
+static constexpr std::string_view kFakeType = "FakeType";
+static constexpr std::string_view kPropertyQualifiedId = "qualifiedId";
+
+// Schema type with nested joinable properties: NestedType
+static constexpr std::string_view kNestedType = "NestedType";
+static constexpr std::string_view kPropertyNestedDoc = "nested";
+static constexpr std::string_view kPropertyQualifiedId2 = "qualifiedId2";
+
+class QualifiedIdJoinIndexingHandlerV3Test : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+
+    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")));
+    }
+
+    base_dir_ = GetTestTempDir() + "/icing_test";
+    ASSERT_THAT(filesystem_.CreateDirectoryRecursively(base_dir_.c_str()),
+                IsTrue());
+
+    qualified_id_join_index_dir_ = base_dir_ + "/qualified_id_join_index";
+    schema_store_dir_ = base_dir_ + "/schema_store";
+    doc_store_dir_ = base_dir_ + "/doc_store";
+
+    ICING_ASSERT_OK_AND_ASSIGN(
+        qualified_id_join_index_,
+        QualifiedIdJoinIndexImplV3::Create(
+            filesystem_, qualified_id_join_index_dir_, *feature_flags_));
+
+    language_segmenter_factory::SegmenterOptions segmenter_options(ULOC_US);
+    ICING_ASSERT_OK_AND_ASSIGN(
+        lang_segmenter_,
+        language_segmenter_factory::Create(std::move(segmenter_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(kReferencedType)
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName(kPropertyName)
+                                     .SetDataTypeString(TERM_MATCH_EXACT,
+                                                        TOKENIZER_PLAIN)
+                                     .SetCardinality(CARDINALITY_OPTIONAL)))
+            .AddType(SchemaTypeConfigBuilder().SetType(kFakeType).AddProperty(
+                PropertyConfigBuilder()
+                    .SetName(kPropertyQualifiedId)
+                    .SetDataTypeJoinableString(JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                    .SetCardinality(CARDINALITY_OPTIONAL)))
+            .AddType(
+                SchemaTypeConfigBuilder()
+                    .SetType(kNestedType)
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName(kPropertyNestedDoc)
+                            .SetDataTypeDocument(
+                                kFakeType, /*index_nested_properties=*/true)
+                            .SetCardinality(CARDINALITY_OPTIONAL))
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName(kPropertyQualifiedId2)
+                                     .SetDataTypeJoinableString(
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                     .SetCardinality(CARDINALITY_OPTIONAL)))
+            .Build();
+    ICING_ASSERT_OK(schema_store_->SetSchema(
+        schema, /*ignore_errors_and_delete_documents=*/false,
+        /*allow_circular_schema_definitions=*/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,
+                              /*initialize_stats=*/nullptr));
+    doc_store_ = std::move(create_result.document_store);
+
+    // Get FakeType related ids.
+    ICING_ASSERT_OK_AND_ASSIGN(fake_type_id_,
+                               schema_store_->GetSchemaTypeId(kFakeType));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        const JoinablePropertyMetadata* metadata1,
+        schema_store_->GetJoinablePropertyMetadata(
+            fake_type_id_, std::string(kPropertyQualifiedId)));
+    ASSERT_THAT(metadata1, NotNull());
+    fake_type_joinable_property_id_ = metadata1->id;
+
+    // Get NestedType related ids.
+    ICING_ASSERT_OK_AND_ASSIGN(nested_type_id_,
+                               schema_store_->GetSchemaTypeId(kNestedType));
+    ICING_ASSERT_OK_AND_ASSIGN(
+        const JoinablePropertyMetadata* metadata2,
+        schema_store_->GetJoinablePropertyMetadata(
+            nested_type_id_,
+            absl_ports::StrCat(kPropertyNestedDoc, ".", kPropertyQualifiedId)));
+    ASSERT_THAT(metadata2, NotNull());
+    nested_type_nested_joinable_property_id_ = metadata2->id;
+    ICING_ASSERT_OK_AND_ASSIGN(
+        const JoinablePropertyMetadata* metadata3,
+        schema_store_->GetJoinablePropertyMetadata(
+            nested_type_id_, std::string(kPropertyQualifiedId2)));
+    ASSERT_THAT(metadata3, NotNull());
+    nested_type_joinable_property_id_ = metadata3->id;
+  }
+
+  void TearDown() override {
+    doc_store_.reset();
+    schema_store_.reset();
+    lang_segmenter_.reset();
+    qualified_id_join_index_.reset();
+
+    filesystem_.DeleteDirectoryRecursively(base_dir_.c_str());
+  }
+
+  std::unique_ptr<FeatureFlags> feature_flags_;
+  Filesystem filesystem_;
+  FakeClock fake_clock_;
+  std::string base_dir_;
+  std::string qualified_id_join_index_dir_;
+  std::string schema_store_dir_;
+  std::string doc_store_dir_;
+
+  std::unique_ptr<QualifiedIdJoinIndexImplV3> qualified_id_join_index_;
+  std::unique_ptr<LanguageSegmenter> lang_segmenter_;
+  std::unique_ptr<SchemaStore> schema_store_;
+  std::unique_ptr<DocumentStore> doc_store_;
+
+  // FakeType related ids.
+  SchemaTypeId fake_type_id_;
+  JoinablePropertyId fake_type_joinable_property_id_;
+
+  // NestedType related ids.
+  SchemaTypeId nested_type_id_;
+  JoinablePropertyId nested_type_nested_joinable_property_id_;
+  JoinablePropertyId nested_type_joinable_property_id_;
+};
+
+TEST_F(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(
+          &fake_clock_, /*doc_store=*/nullptr, qualified_id_join_index_.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));
+}
+
+TEST_F(QualifiedIdJoinIndexingHandlerV3Test, HandleJoinableProperty) {
+  // Create and put 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(DocumentStore::PutResult parent_put_result,
+                             doc_store_->Put(parent_document));
+
+  // Create and put child document. Also tokenize it.
+  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(child_document));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      TokenizedDocument tokenized_document,
+      TokenizedDocument::Create(schema_store_.get(), lang_segmenter_.get(),
+                                std::move(child_document)));
+
+  // Handle 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, 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),
+      IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
+          child_put_result.new_document_id, fake_type_joinable_property_id_))));
+}
+
+TEST_F(QualifiedIdJoinIndexingHandlerV3Test, HandleNestedJoinableProperty) {
+  // Create and put parent document1. Get its document id and namespace id.
+  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(DocumentStore::PutResult parent_put_result1,
+                             doc_store_->Put(parent_document1));
+
+  // Create and put parent document2.
+  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(DocumentStore::PutResult parent_put_result2,
+                             doc_store_->Put(parent_document2));
+
+  // Create and put child document:
+  // - kPropertyNestedDoc.kPropertyQualifiedId refers to parent_document2.
+  // - kPropertyQualifiedId2 refers to parent_document1.
+  //
+  // Also tokenize it.
+  DocumentProto child_document =
+      DocumentBuilder()
+          .SetKey("pkg$db/ns", "nested_type/1")
+          .SetSchema(std::string(kNestedType))
+          .AddDocumentProperty(
+              std::string(kPropertyNestedDoc),
+              DocumentBuilder()
+                  .SetKey("pkg$db/ns", "nested_fake_type/1")
+                  .SetSchema(std::string(kFakeType))
+                  .AddStringProperty(std::string(kPropertyQualifiedId),
+                                     "pkg$db/ns#ref_type/2")
+                  .Build())
+          .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));
+
+  // Handle nested_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, 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),
+              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),
+              IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
+                  child_put_result.new_document_id,
+                  nested_type_nested_joinable_property_id_))));
+}
+
+TEST_F(QualifiedIdJoinIndexingHandlerV3Test,
+       HandleShouldSkipInvalidFormatQualifiedId) {
+  static constexpr std::string_view kInvalidFormatQualifiedId =
+      "invalid_format_qualified_id";
+  ASSERT_THAT(QualifiedId::Parse(kInvalidFormatQualifiedId),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+
+  // Create and put child document with an invalid format referenced qualified
+  // id. Also tokenize it.
+  DocumentProto document =
+      DocumentBuilder()
+          .SetKey("icing", "fake_type/1")
+          .SetSchema(std::string(kFakeType))
+          .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));
+
+  // Handle document. Should ignore invalid format qualified id.
+  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, 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(). Index data
+  // should remain unchanged since there is no valid qualified id, but
+  // last_added_document_id should be updated.
+  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()));
+}
+
+TEST_F(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));
+  ASSERT_THAT(tokenized_document.qualified_id_join_properties(), IsEmpty());
+
+  // Handle 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, 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(). Index data
+  // should remain unchanged since there is no qualified id, but
+  // last_added_document_id should be updated.
+  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()));
+}
+
+TEST_F(QualifiedIdJoinIndexingHandlerV3Test, HandleShouldMigrateParent) {
+  // Create and put 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(DocumentStore::PutResult parent_put_result,
+                             doc_store_->Put(parent_document));
+
+  // Create and put child and grandchild document with relations:
+  // parent_document <- child_document <- grandchild_document
+  // Also tokenize them.
+  DocumentProto child_document =
+      DocumentBuilder()
+          .SetKey("icing", "fake_type/1")
+          .SetSchema(std::string(kFakeType))
+          .AddStringProperty(std::string(kPropertyQualifiedId),
+                             "pkg$db/ns#ref_type/1")
+          .Build();
+  DocumentProto grandchild_document =
+      DocumentBuilder()
+          .SetKey("icing", "fake_type/2")
+          .SetSchema(std::string(kFakeType))
+          .AddStringProperty(std::string(kPropertyQualifiedId),
+                             "icing#fake_type/1")
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
+      QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
+                                             qualified_id_join_index_.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));
+  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)));
+  ICING_ASSERT_OK(handler->Handle(
+      grandchild_tokenized_document, grandchild_put_result.new_document_id,
+      grandchild_put_result.old_document_id,
+      /*recovery_mode=*/false, /*put_document_stats=*/nullptr));
+
+  // Sanity check: parent contains child join id pair and child contains
+  // grandchild join id pair.
+  ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
+              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),
+      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),
+              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));
+  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(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),
+              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),
+              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),
+              IsOkAndHolds(ElementsAre(
+                  DocumentJoinIdPair(grandchild_put_result.new_document_id,
+                                     fake_type_joinable_property_id_))));
+}
+
+TEST_F(QualifiedIdJoinIndexingHandlerV3Test,
+       HandleInvalidNewDocumentIdShouldReturnInvalidArgumentError) {
+  // Create and put 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(DocumentStore::PutResult parent_put_result,
+                             doc_store_->Put(parent_document));
+
+  // Create and put child document. Also tokenize it.
+  DocumentProto 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)));
+
+  qualified_id_join_index_->set_last_added_document_id(
+      parent_put_result.new_document_id);
+  ASSERT_THAT(qualified_id_join_index_->last_added_document_id(),
+              Eq(parent_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()));
+
+  // Handling document with kInvalidDocumentId should cause a failure.
+  EXPECT_THAT(
+      handler->Handle(tokenized_document, kInvalidDocumentId,
+                      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(parent_put_result.new_document_id));
+  EXPECT_THAT(qualified_id_join_index_, Pointee(IsEmpty()));
+
+  // Recovery mode should get the same result.
+  EXPECT_THAT(
+      handler->Handle(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),
+              IsOkAndHolds(IsEmpty()));
+}
+
+TEST_F(QualifiedIdJoinIndexingHandlerV3Test,
+       HandleOutOfOrderDocumentIdShouldReturnInvalidArgumentError) {
+  // Create and put 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(DocumentStore::PutResult parent_put_result,
+                             doc_store_->Put(parent_document));
+
+  // Create and put child document. Also tokenize it.
+  DocumentProto 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)));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
+      QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
+                                             qualified_id_join_index_.get()));
+
+  // Handling document with document_id == last_added_document_id should cause a
+  // failure.
+  qualified_id_join_index_->set_last_added_document_id(
+      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));
+  // 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),
+              IsOkAndHolds(IsEmpty()));
+
+  // Handling document with document_id < last_added_document_id should cause a
+  // failure.
+  qualified_id_join_index_->set_last_added_document_id(
+      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));
+  // 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),
+              IsOkAndHolds(IsEmpty()));
+}
+
+TEST_F(QualifiedIdJoinIndexingHandlerV3Test,
+       HandleRecoveryModeShouldIndexDocsGtLastAddedDocId) {
+  // Create and put 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(DocumentStore::PutResult parent_put_result,
+                             doc_store_->Put(parent_document));
+
+  // Create and put child document. Also tokenize it.
+  DocumentProto 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)));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
+      QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
+                                             qualified_id_join_index_.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.
+  qualified_id_join_index_->set_last_added_document_id(
+      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(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),
+      IsOkAndHolds(ElementsAre(DocumentJoinIdPair(
+          child_put_result.new_document_id, fake_type_joinable_property_id_))));
+}
+
+TEST_F(QualifiedIdJoinIndexingHandlerV3Test,
+       HandleRecoveryModeShouldIgnoreDocsLeLastAddedDocId) {
+  // Create and put 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(DocumentStore::PutResult parent_put_result,
+                             doc_store_->Put(parent_document));
+
+  // Create and put child document. Also tokenize it.
+  DocumentProto 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)));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<QualifiedIdJoinIndexingHandler> handler,
+      QualifiedIdJoinIndexingHandler::Create(&fake_clock_, doc_store_.get(),
+                                             qualified_id_join_index_.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
+  // document, so both index data and last_added_document_id should remain
+  // unchanged.
+  qualified_id_join_index_->set_last_added_document_id(
+      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(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),
+              IsOkAndHolds(IsEmpty()));
+
+  // Handle document with document_id < last_added_document_id in recovery mode.
+  // We should not get any error, but the handler should ignore the document, so
+  // both index data and last_added_document_id should remain unchanged.
+  qualified_id_join_index_->set_last_added_document_id(
+      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(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),
+              IsOkAndHolds(IsEmpty()));
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/join/qualified-id-join-indexing-handler.cc b/icing/join/qualified-id-join-indexing-handler.cc
index df86cba..14d8231 100644
--- a/icing/join/qualified-id-join-indexing-handler.cc
+++ b/icing/join/qualified-id-join-indexing-handler.cc
@@ -25,7 +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/join/doc-join-info.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/legacy/core/icing-string-util.h"
@@ -34,7 +34,7 @@
 #include "icing/store/document-filter-data.h"
 #include "icing/store/document-id.h"
 #include "icing/store/document-store.h"
-#include "icing/store/namespace-fingerprint-identifier.h"
+#include "icing/store/namespace-id-fingerprint.h"
 #include "icing/store/namespace-id.h"
 #include "icing/util/clock.h"
 #include "icing/util/logging.h"
@@ -60,7 +60,8 @@
 
 libtextclassifier3::Status QualifiedIdJoinIndexingHandler::Handle(
     const TokenizedDocument& tokenized_document, DocumentId document_id,
-    bool recovery_mode, PutDocumentStatsProto* put_document_stats) {
+    DocumentId old_document_id, bool recovery_mode,
+    PutDocumentStatsProto* put_document_stats) {
   std::unique_ptr<Timer> index_timer = clock_.GetNewTimer();
 
   if (!IsDocumentIdValid(document_id)) {
@@ -81,90 +82,17 @@
   }
   qualified_id_join_index_.set_last_added_document_id(document_id);
 
-  if (qualified_id_join_index_.is_v2()) {
-    // v2
-    std::optional<DocumentFilterData> filter_data =
-        doc_store_.GetAliveDocumentFilterData(
-            document_id,
-            /*current_time_ms=*/std::numeric_limits<int64_t>::min());
-    if (!filter_data) {
-      // This should not happen.
-      return absl_ports::InternalError(
-          "Failed to get alive document filter data when indexing");
-    }
-
-    for (const JoinableProperty<std::string_view>& qualified_id_property :
-         tokenized_document.qualified_id_join_properties()) {
-      // Parse all qualified id strings and convert them to
-      // NamespaceFingerprintIdentifier.
-      std::vector<NamespaceFingerprintIdentifier> ref_doc_ns_fingerprint_ids;
-      for (std::string_view ref_qualified_id_str :
-           qualified_id_property.values) {
-        // Attempt to parse qualified id string to make sure the format is
-        // correct.
-        auto ref_qualified_id_or = QualifiedId::Parse(ref_qualified_id_str);
-        if (!ref_qualified_id_or.ok()) {
-          // Skip incorrect format of qualified id string.
-          continue;
-        }
-
-        QualifiedId ref_qualified_id =
-            std::move(ref_qualified_id_or).ValueOrDie();
-        auto ref_namespace_id_or =
-            doc_store_.GetNamespaceId(ref_qualified_id.name_space());
-        if (!ref_namespace_id_or.ok()) {
-          // Skip invalid namespace id.
-          continue;
-        }
-        NamespaceId ref_namespace_id =
-            std::move(ref_namespace_id_or).ValueOrDie();
-
-        ref_doc_ns_fingerprint_ids.push_back(NamespaceFingerprintIdentifier(
-            ref_namespace_id, ref_qualified_id.uri()));
-      }
-
-      // Batch add all join data of this (schema_type_id, joinable_property_id)
-      // into to the index.
-      libtextclassifier3::Status status = qualified_id_join_index_.Put(
-          filter_data->schema_type_id(), qualified_id_property.metadata.id,
-          document_id, std::move(ref_doc_ns_fingerprint_ids));
-      if (!status.ok()) {
-        ICING_LOG(WARNING)
-            << "Failed to add data into qualified id join index v2 due to: "
-            << status.error_message();
-        return status;
-      }
-    }
-  } else {
-    // v1
-    // TODO(b/275121148): deprecate this part after rollout v2.
-    for (const JoinableProperty<std::string_view>& qualified_id_property :
-         tokenized_document.qualified_id_join_properties()) {
-      if (qualified_id_property.values.empty()) {
-        continue;
-      }
-
-      DocJoinInfo info(document_id, qualified_id_property.metadata.id);
-      // Currently we only support single (non-repeated) joinable value under a
-      // property.
-      std::string_view ref_qualified_id_str = qualified_id_property.values[0];
-
-      // Attempt to parse qualified id string to make sure the format is
-      // correct.
-      if (!QualifiedId::Parse(ref_qualified_id_str).ok()) {
-        // Skip incorrect format of qualified id string to save disk space.
-        continue;
-      }
-
-      libtextclassifier3::Status status =
-          qualified_id_join_index_.Put(info, ref_qualified_id_str);
-      if (!status.ok()) {
-        ICING_LOG(WARNING)
-            << "Failed to add data into qualified id join index due to: "
-            << status.error_message();
-        return status;
-      }
-    }
+  switch (qualified_id_join_index_.version()) {
+    case QualifiedIdJoinIndex::Version::kV1:
+      ICING_RETURN_IF_ERROR(HandleV1(tokenized_document, document_id));
+      break;
+    case QualifiedIdJoinIndex::Version::kV2:
+      ICING_RETURN_IF_ERROR(HandleV2(tokenized_document, document_id));
+      break;
+    case QualifiedIdJoinIndex::Version::kV3:
+      ICING_RETURN_IF_ERROR(
+          HandleV3(tokenized_document, document_id, old_document_id));
+      break;
   }
 
   if (put_document_stats != nullptr) {
@@ -175,5 +103,155 @@
   return libtextclassifier3::Status::OK;
 }
 
+libtextclassifier3::Status QualifiedIdJoinIndexingHandler::HandleV1(
+    const TokenizedDocument& tokenized_document, DocumentId document_id) {
+  for (const JoinableProperty<std::string_view>& qualified_id_property :
+       tokenized_document.qualified_id_join_properties()) {
+    if (qualified_id_property.values.empty()) {
+      continue;
+    }
+
+    DocumentJoinIdPair document_join_id_pair(document_id,
+                                             qualified_id_property.metadata.id);
+    // Currently we only support single (non-repeated) joinable value under a
+    // property.
+    std::string_view ref_qualified_id_str = qualified_id_property.values[0];
+
+    // Attempt to parse qualified id string to make sure the format is
+    // correct.
+    if (!QualifiedId::Parse(ref_qualified_id_str).ok()) {
+      // Skip incorrect format of qualified id string to save disk space.
+      continue;
+    }
+
+    libtextclassifier3::Status status = qualified_id_join_index_.Put(
+        document_join_id_pair, ref_qualified_id_str);
+    if (!status.ok()) {
+      ICING_LOG(WARNING)
+          << "Failed to add data into qualified id join index due to: "
+          << status.error_message();
+      return status;
+    }
+  }
+  return libtextclassifier3::Status::OK;
+}
+
+libtextclassifier3::Status QualifiedIdJoinIndexingHandler::HandleV2(
+    const TokenizedDocument& tokenized_document, DocumentId document_id) {
+  std::optional<DocumentFilterData> filter_data =
+      doc_store_.GetAliveDocumentFilterData(
+          document_id,
+          /*current_time_ms=*/std::numeric_limits<int64_t>::min());
+  if (!filter_data) {
+    // This should not happen.
+    return absl_ports::InternalError(
+        "Failed to get alive document filter data when indexing");
+  }
+
+  for (const JoinableProperty<std::string_view>& qualified_id_property :
+       tokenized_document.qualified_id_join_properties()) {
+    // Parse all qualified id strings and convert them to
+    // NamespaceIdFingerprint.
+    std::vector<NamespaceIdFingerprint> ref_doc_nsid_uri_fingerprints;
+    for (std::string_view ref_qualified_id_str : qualified_id_property.values) {
+      // Attempt to parse qualified id string to make sure the format is
+      // correct.
+      auto ref_qualified_id_or = QualifiedId::Parse(ref_qualified_id_str);
+      if (!ref_qualified_id_or.ok()) {
+        // Skip incorrect format of qualified id string.
+        continue;
+      }
+
+      QualifiedId ref_qualified_id =
+          std::move(ref_qualified_id_or).ValueOrDie();
+      auto ref_namespace_id_or =
+          doc_store_.GetNamespaceId(ref_qualified_id.name_space());
+      if (!ref_namespace_id_or.ok()) {
+        // Skip invalid namespace id.
+        continue;
+      }
+      NamespaceId ref_namespace_id =
+          std::move(ref_namespace_id_or).ValueOrDie();
+
+      ref_doc_nsid_uri_fingerprints.push_back(
+          NamespaceIdFingerprint(ref_namespace_id, ref_qualified_id.uri()));
+    }
+
+    // Batch add all join data of this (schema_type_id, joinable_property_id)
+    // into to the index.
+    libtextclassifier3::Status status = qualified_id_join_index_.Put(
+        filter_data->schema_type_id(), qualified_id_property.metadata.id,
+        document_id, std::move(ref_doc_nsid_uri_fingerprints));
+    if (!status.ok()) {
+      ICING_LOG(WARNING)
+          << "Failed to add data into qualified id join index v2 due to: "
+          << status.error_message();
+      return status;
+    }
+  }
+  return libtextclassifier3::Status::OK;
+}
+
+libtextclassifier3::Status QualifiedIdJoinIndexingHandler::HandleV3(
+    const TokenizedDocument& tokenized_document, DocumentId document_id,
+    DocumentId old_document_id) {
+  // (Parent perspective)
+  // When replacement, if there were any existing child documents joining to it,
+  // then we need to migrate the old document id to the new document id.
+  if (IsDocumentIdValid(old_document_id)) {
+    ICING_RETURN_IF_ERROR(
+        qualified_id_join_index_.MigrateParent(old_document_id, document_id));
+  }
+
+  // (Child perspective)
+  // Add child join data.
+  for (const JoinableProperty<std::string_view>& qualified_id_property :
+       tokenized_document.qualified_id_join_properties()) {
+    if (qualified_id_property.values.empty()) {
+      continue;
+    }
+
+    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());
+    for (std::string_view parent_qualified_id_str :
+         qualified_id_property.values) {
+      libtextclassifier3::StatusOr<QualifiedId> parent_qualified_id_or =
+          QualifiedId::Parse(parent_qualified_id_str);
+      if (!parent_qualified_id_or.ok()) {
+        // Skip incorrect format of qualified id string.
+        continue;
+      }
+      QualifiedId parent_qualified_id =
+          std::move(parent_qualified_id_or).ValueOrDie();
+
+      // Lookup document store to get the parent document id.
+      libtextclassifier3::StatusOr<DocumentId> parent_doc_id_or =
+          doc_store_.GetDocumentId(parent_qualified_id.name_space(),
+                                   parent_qualified_id.uri());
+      if (!parent_doc_id_or.ok() ||
+          parent_doc_id_or.ValueOrDie() == kInvalidDocumentId) {
+        // Skip invalid parent document id or parent document does not exist.
+        continue;
+      }
+      parent_doc_ids.push_back(parent_doc_id_or.ValueOrDie());
+    }
+
+    // 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));
+    if (!status.ok()) {
+      ICING_LOG(WARNING)
+          << "Failed to add data into qualified id join index due to: "
+          << status.error_message();
+      return status;
+    }
+  }
+  return libtextclassifier3::Status::OK;
+}
+
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/join/qualified-id-join-indexing-handler.h b/icing/join/qualified-id-join-indexing-handler.h
index 8a11bf9..53c166c 100644
--- a/icing/join/qualified-id-join-indexing-handler.h
+++ b/icing/join/qualified-id-join-indexing-handler.h
@@ -49,6 +49,14 @@
   // Handles the joinable qualified id data indexing process: add data into the
   // qualified id join index.
   //
+  // old_document_id:
+  // - It is only used in QualifiedIdJoinIndexImplV3: if we update a parent
+  //   document, then the existing join data for the parent document should be
+  //   migrated from old_document_id to (new) document_id.
+  // - For other join index versions, since we store (namespace id,
+  //   fingerprint(uri)) or the raw qualified id string for the parent, there is
+  //   no need to migrate.
+  //
   /// Returns:
   //   - OK on success.
   //   - INVALID_ARGUMENT_ERROR if document_id is invalid OR document_id is less
@@ -58,7 +66,8 @@
   //   - Any QualifiedIdJoinIndex errors.
   libtextclassifier3::Status Handle(
       const TokenizedDocument& tokenized_document, DocumentId document_id,
-      bool recovery_mode, PutDocumentStatsProto* put_document_stats) override;
+      DocumentId old_document_id, bool recovery_mode,
+      PutDocumentStatsProto* put_document_stats) override;
 
  private:
   explicit QualifiedIdJoinIndexingHandler(
@@ -68,6 +77,21 @@
         doc_store_(*doc_store),
         qualified_id_join_index_(*qualified_id_join_index) {}
 
+  // TODO(b/275121148): deprecate v1, v2 after rollout v3.
+
+  // Helper function to handle indexing for QualfiedIdJoinIndexImplV1.
+  libtextclassifier3::Status HandleV1(
+      const TokenizedDocument& tokenized_document, DocumentId document_id);
+
+  // Helper function to handle indexing for QualfiedIdJoinIndexImplV2.
+  libtextclassifier3::Status HandleV2(
+      const TokenizedDocument& tokenized_document, DocumentId document_id);
+
+  // Helper function to handle indexing for QualfiedIdJoinIndexImplV3.
+  libtextclassifier3::Status HandleV3(
+      const TokenizedDocument& tokenized_document, DocumentId document_id,
+      DocumentId old_document_id);
+
   const DocumentStore& doc_store_;                 // Does not own.
   QualifiedIdJoinIndex& qualified_id_join_index_;  // Does not own.
 };
diff --git a/icing/legacy/index/icing-dynamic-trie.cc b/icing/legacy/index/icing-dynamic-trie.cc
index 4514385..e665272 100644
--- a/icing/legacy/index/icing-dynamic-trie.cc
+++ b/icing/legacy/index/icing-dynamic-trie.cc
@@ -284,7 +284,11 @@
   const Node *GetNode(uint32_t idx) const {
     return &array_storage_[NODE].array_cast<Node>()[idx];
   }
+
+  // REQUIRES: !empty(). Otherwise node 0 could contain invalid data
+  //   (next_index, is_leaf, log2_num_children).
   const Node *GetRootNode() const { return GetNode(0); }
+
   const Next *GetNext(uint32_t idx, int child) const {
     return &array_storage_[NEXT].array_cast<Next>()[idx + child];
   }
@@ -336,6 +340,8 @@
 
   void inc_num_keys() { hdr_.hdr.set_num_keys(hdr_.hdr.num_keys() + 1); }
 
+  void dec_num_keys() { hdr_.hdr.set_num_keys(hdr_.hdr.num_keys() - 1); }
+
  private:
   friend void IcingDynamicTrie::SetHeader(
       const IcingDynamicTrieHeader &new_hdr);
@@ -1072,9 +1078,22 @@
   //   level - how many levels deep we are in the trie
   //   ret - the stream to pretty print to
   //   keys - the keys encountered are appended to this
+  //
+  // REQUIRES: node is valid.
+  //   - Since we only invalidate Next to remove the edge from the trie and Node
+  //     is not invalidated after deletion, the caller should ensure that it
+  //     traverses correctly to a valid node according to the trie structure.
+  //     Calling this function with an invalid node is undefined behavior since
+  //     it could traverse into a deleted subtree, or invalid memory addresses.
+  //   - This also means storage_->empty() should be checked before calling this
+  //     function with the root node.
   void DumpNodeRecursive(const std::string &prefix, const Node &node, int level,
                          std::ostream *ret,
                          std::vector<std::string> *keys) const {
+    // This function should be called only if the node is valid. The first call
+    // is always from the root node, so it means the trie should not be empty at
+    // this moment. Otherwise (root) node could contain invalid next_index(),
+    // is_leaf(), and log2_num_children().
     if (node.is_leaf()) {
       // Dump suffix and value.
       for (int i = 0; i < level; i++) {
@@ -1368,8 +1387,19 @@
   return storage_->hdr().num_keys();
 }
 
+bool IcingDynamicTrie::empty() const {
+  if (!is_initialized()) {
+    ICING_LOG(FATAL) << "DynamicTrie not initialized";
+  }
+  return storage_->empty();
+}
+
 void IcingDynamicTrie::CollectStatsRecursive(const Node &node, Stats *stats,
                                              uint32_t depth) const {
+  // This function should be called only if the node is valid. The first call is
+  // always from the root node, so it means the trie should not be empty at this
+  // moment. Otherwise (root) node could contain invalid next_index(),
+  // is_leaf(), and log2_num_children().
   if (node.is_leaf()) {
     stats->num_leaves++;
     stats->sum_depth += depth;
@@ -2577,6 +2607,19 @@
 // 2. Remove the suffix and the value.
 // 3. Reset the nexts that point to the nodes to be removed.
 // 4. Sort any next array if needed.
+// 5. Reset the trie state if the trie is empty after deletion.
+//    - This is essential for storage_->empty(), which is a critical check for
+//      all trie APIs before accessing the root node via
+//      storage_->GetRootNode().
+//    - When the trie is empty, it is possible that the root node (i.e.
+//      Node(0)):
+//      - Contains an invalid next_index(), and accessing it will cause a crash
+//        or fetch incorrect data.
+//      - Points to a valid next array but the next elements in the array
+//        contain kInvalidNodeIndex. Accessing the next node via the next
+//        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) {
   if (!is_initialized()) {
     ICING_LOG(ERROR) << "DynamicTrie not initialized";
@@ -2688,6 +2731,19 @@
     }
   }
 
+  storage_->dec_num_keys();
+  if (storage_->hdr().num_keys() == 0) {
+    // Reset the trie state to empty by calling Clear() directly.
+    //
+    // Note: in this case, last_multichild_node will be nullptr as well.
+    // - If we never saw a node with multiple children before deletion, then all
+    //   the traversed nodes, including the root node, are single-child nodes
+    //   before deletion.
+    // - Therefore, after deletion, there should be no valid nodes or nexts in
+    //   the trie.
+    Clear();
+  }
+
   return true;
 }
 
diff --git a/icing/legacy/index/icing-dynamic-trie.h b/icing/legacy/index/icing-dynamic-trie.h
index 301ee24..53d9bad 100644
--- a/icing/legacy/index/icing-dynamic-trie.h
+++ b/icing/legacy/index/icing-dynamic-trie.h
@@ -284,6 +284,8 @@
   // Number of keys in trie.
   uint32_t size() const;
 
+  bool empty() const;
+
   // Collecting stats.
   void CollectStats(Stats *stats) const;
 
@@ -615,6 +617,15 @@
   static const uint32_t kInvalidSuffixIndex;
 
   // Stats helpers.
+  //
+  // REQUIRES: node is valid.
+  //   - Since we only invalidate Next to remove the edge from the trie and Node
+  //     is not invalidated after deletion, the caller should ensure that it
+  //     traverses correctly to a valid node according to the trie structure.
+  //     Calling this function with an invalid node is undefined behavior since
+  //     it could traverse into a deleted subtree, or invalid memory addresses.
+  //   - This also means storage_->empty() should be checked before calling this
+  //     function with the root node.
   void CollectStatsRecursive(const Node &node, Stats *stats,
                              uint32_t depth = 0) const;
 
diff --git a/icing/legacy/index/icing-dynamic-trie_test.cc b/icing/legacy/index/icing-dynamic-trie_test.cc
index 2b5a8b9..778a1a9 100644
--- a/icing/legacy/index/icing-dynamic-trie_test.cc
+++ b/icing/legacy/index/icing-dynamic-trie_test.cc
@@ -40,7 +40,9 @@
 using testing::ContainerEq;
 using testing::ElementsAre;
 using testing::Eq;
+using testing::IsEmpty;
 using testing::Not;
+using testing::SizeIs;
 
 constexpr std::string_view kKeys[] = {
     "", "ab", "ac", "abd", "bac", "bb", "bacd", "abbb", "abcdefg",
@@ -946,10 +948,14 @@
   uint32_t value = 1;
   ASSERT_THAT(trie.Insert("foo", &value), IsOk());
   ASSERT_TRUE(trie.Find("foo", &value));
+  ASSERT_THAT(trie, SizeIs(1));
+  ASSERT_THAT(trie, Not(IsEmpty()));
 
   // Deletes the key.
   EXPECT_TRUE(trie.Delete("foo"));
   EXPECT_FALSE(trie.Find("foo", &value));
+  EXPECT_THAT(trie, SizeIs(0));  // Explicitly test size() method.
+  EXPECT_THAT(trie, IsEmpty());
 }
 
 TEST_F(IcingDynamicTrieTest, DeletionShouldWorkWhenLastCharIsLeaf) {
@@ -972,11 +978,15 @@
   ASSERT_THAT(trie.Insert("ba", &value), IsOk());
   ASSERT_TRUE(trie.Find("bar", &value));
   ASSERT_TRUE(trie.Find("ba", &value));
+  ASSERT_THAT(trie, SizeIs(2));
+  ASSERT_THAT(trie, Not(IsEmpty()));
 
   // Deletes "bar". "r" is a leaf node in the trie.
   EXPECT_TRUE(trie.Delete("bar"));
   EXPECT_FALSE(trie.Find("bar", &value));
   EXPECT_TRUE(trie.Find("ba", &value));
+  EXPECT_THAT(trie, SizeIs(1));
+  EXPECT_THAT(trie, Not(IsEmpty()));
 }
 
 TEST_F(IcingDynamicTrieTest, DeletionShouldWorkWithTerminationNode) {
@@ -999,11 +1009,15 @@
   ASSERT_THAT(trie.Insert("ba", &value), IsOk());
   ASSERT_TRUE(trie.Find("bar", &value));
   ASSERT_TRUE(trie.Find("ba", &value));
+  ASSERT_THAT(trie, SizeIs(2));
+  ASSERT_THAT(trie, Not(IsEmpty()));
 
   // Deletes "ba" which is a key with termination node in the trie.
   EXPECT_TRUE(trie.Delete("ba"));
   EXPECT_FALSE(trie.Find("ba", &value));
   EXPECT_TRUE(trie.Find("bar", &value));
+  EXPECT_THAT(trie, SizeIs(1));
+  EXPECT_THAT(trie, Not(IsEmpty()));
 }
 
 TEST_F(IcingDynamicTrieTest, DeletionShouldWorkWithMultipleNexts) {
@@ -1028,6 +1042,8 @@
   ASSERT_TRUE(trie.Find("bb", &value));
   ASSERT_TRUE(trie.Find("bc", &value));
   ASSERT_TRUE(trie.Find("bd", &value));
+  ASSERT_THAT(trie, SizeIs(4));
+  ASSERT_THAT(trie, Not(IsEmpty()));
 
   // Deletes "bc".
   EXPECT_TRUE(trie.Delete("bc"));
@@ -1035,6 +1051,8 @@
   EXPECT_TRUE(trie.Find("ba", &value));
   EXPECT_TRUE(trie.Find("bb", &value));
   EXPECT_TRUE(trie.Find("bd", &value));
+  EXPECT_THAT(trie, SizeIs(3));
+  EXPECT_THAT(trie, Not(IsEmpty()));
 }
 
 TEST_F(IcingDynamicTrieTest, DeletionShouldWorkWithMultipleTrieBranches) {
@@ -1065,12 +1083,49 @@
   ASSERT_TRUE(trie.Find("batter", &value));
   ASSERT_TRUE(trie.Find("battle", &value));
   ASSERT_TRUE(trie.Find("bar", &value));
+  ASSERT_THAT(trie, SizeIs(3));
+  ASSERT_THAT(trie, Not(IsEmpty()));
 
   // Deletes "batter".
   EXPECT_TRUE(trie.Delete("batter"));
   EXPECT_FALSE(trie.Find("batter", &value));
   EXPECT_TRUE(trie.Find("battle", &value));
   EXPECT_TRUE(trie.Find("bar", &value));
+  EXPECT_THAT(trie, SizeIs(2));
+  EXPECT_THAT(trie, Not(IsEmpty()));
+}
+
+TEST_F(IcingDynamicTrieTest, DeletionShouldResetEmptyStateIfAllKeysAreDeleted) {
+  IcingFilesystem filesystem;
+  IcingDynamicTrie trie(trie_files_prefix_, IcingDynamicTrie::RuntimeOptions(),
+                        &filesystem);
+  ASSERT_TRUE(trie.CreateIfNotExist(IcingDynamicTrie::Options()));
+  ASSERT_TRUE(trie.Init());
+
+  uint32_t value1 = 1;
+  ASSERT_THAT(trie.Insert("foo", &value1), IsOk());
+
+  uint32_t value2 = 2;
+  ASSERT_THAT(trie.Insert("bar", &value2), IsOk());
+
+  uint32_t value3 = 3;
+  ASSERT_THAT(trie.Insert("baz", &value3), IsOk());
+
+  ASSERT_THAT(trie, SizeIs(3));
+  ASSERT_THAT(trie, Not(IsEmpty()));
+
+  // Delete "foo", "bar", "baz".
+  EXPECT_TRUE(trie.Delete("foo"));
+  EXPECT_TRUE(trie.Delete("bar"));
+  EXPECT_TRUE(trie.Delete("baz"));
+
+  EXPECT_THAT(trie, SizeIs(0));  // Explicitly test size() method.
+  EXPECT_THAT(trie, IsEmpty());
+
+  uint32_t value;
+  EXPECT_FALSE(trie.Find("foo", &value));
+  EXPECT_FALSE(trie.Find("bar", &value));
+  EXPECT_FALSE(trie.Find("baz", &value));
 }
 
 TEST_F(IcingDynamicTrieTest, InsertionShouldWorkAfterDeletion) {
@@ -1130,6 +1185,43 @@
   EXPECT_THAT(results, ElementsAre("bar"));
 }
 
+TEST_F(IcingDynamicTrieTest, IteratorShouldWorkAfterAllKeysAreDeleted) {
+  IcingFilesystem filesystem;
+  IcingDynamicTrie trie(trie_files_prefix_, IcingDynamicTrie::RuntimeOptions(),
+                        &filesystem);
+  ASSERT_TRUE(trie.CreateIfNotExist(IcingDynamicTrie::Options()));
+  ASSERT_TRUE(trie.Init());
+
+  // Inserts some keys.
+  uint32_t value = 1;
+  ASSERT_THAT(trie.Insert("bar", &value), IsOk());
+  ASSERT_THAT(trie.Insert("bed", &value), IsOk());
+  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"));
+
+  EXPECT_THAT(trie, IsEmpty());
+
+  // Iterates through all keys
+  IcingDynamicTrie::Iterator iterator_all(trie, "");
+  std::vector<std::string> results;
+  for (; iterator_all.IsValid(); iterator_all.Advance()) {
+    results.push_back(std::string(iterator_all.GetKey()));
+  }
+  EXPECT_THAT(results, IsEmpty());
+
+  // Iterates through keys that start with "b"
+  IcingDynamicTrie::Iterator iterator_b(trie, "b");
+  results.clear();
+  for (; iterator_b.IsValid(); iterator_b.Advance()) {
+    results.push_back(std::string(iterator_b.GetKey()));
+  }
+  EXPECT_THAT(results, IsEmpty());
+}
+
 TEST_F(IcingDynamicTrieTest, DeletingNonExistingKeyShouldReturnTrue) {
   IcingFilesystem filesystem;
   IcingDynamicTrie trie(trie_files_prefix_, IcingDynamicTrie::RuntimeOptions(),
diff --git a/icing/monkey_test/icing-monkey-test-runner.cc b/icing/monkey_test/icing-monkey-test-runner.cc
index bc04089..14b4e02 100644
--- a/icing/monkey_test/icing-monkey-test-runner.cc
+++ b/icing/monkey_test/icing-monkey-test-runner.cc
@@ -551,6 +551,8 @@
   // flip this flag to test document store's compatibility.
   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_ = std::make_unique<IcingSearchEngine>(icing_options);
   ASSERT_THAT(icing_->Initialize().status(), ProtoIsOk());
 }
diff --git a/icing/portable/equals-proto.h b/icing/portable/equals-proto.h
index 8bb835e..15a0e61 100644
--- a/icing/portable/equals-proto.h
+++ b/icing/portable/equals-proto.h
@@ -23,7 +23,6 @@
 #include "gmock/gmock.h"          // IWYU pragma: export
 #include <google/protobuf/message_lite.h>  // IWYU pragma: export
 
-#if defined(__ANDROID__) || defined(__APPLE__)
 namespace icing {
 namespace lib {
 namespace portable_equals_proto {
@@ -31,24 +30,29 @@
 // Hence, there is no better way to compare two protos of an arbitrary type.
 // This matcher enables comparing non-google3 protos on, e.g., Android, with
 // a known caveat that it is unable to provide detailed difference information.
+#if defined(__ANDROID__) || defined(__APPLE__)
+
 MATCHER_P(EqualsProto, other, "Compare MessageLite by serialized string") {
   return ::testing::ExplainMatchResult(::testing::Eq(other.SerializeAsString()),
                                        arg.SerializeAsString(),
                                        result_listener);
-}  // MATCHER_P
-}  // namespace portable_equals_proto
-}  // namespace lib
-}  // namespace icing
+}
+
+MATCHER(EqualsProto, "") {
+  return ::testing::ExplainMatchResult(EqualsProto(std::get<1>(arg)),
+                                       std::get<0>(arg), result_listener);
+}
+
 #else
-namespace icing {
-namespace lib {
-namespace portable_equals_proto {
+
 // Leverage the powerful google3 matcher when available, for human readable
 // differences.
 using ::testing::EqualsProto;
+
+#endif  // defined(__ANDROID__) || defined(__APPLE__)
+
 }  // namespace portable_equals_proto
 }  // namespace lib
 }  // namespace icing
-#endif  // defined(__ANDROID__) || defined(__APPLE__)
 
 #endif  // ICING_PORTABLE_EQUALS_PROTO_H_
diff --git a/icing/portable/platform.h b/icing/portable/platform.h
index f7d8734..245e41c 100644
--- a/icing/portable/platform.h
+++ b/icing/portable/platform.h
@@ -44,8 +44,14 @@
   return !IsReverseJniTokenization() && !IsCfStringTokenization();
 }
 
+// ICU and Reverse JNI tokenization are enabled.
+inline bool IsIcuWithReverseTokenization() {
+  return IsReverseJniTokenization() && !IsCfStringTokenization();
+}
+
 inline int GetIcuTokenizationVersion() {
-  return IsIcuTokenization() ? U_ICU_VERSION_MAJOR_NUM : 0;
+  return (IsIcuTokenization() || IsIcuWithReverseTokenization())
+      ? U_ICU_VERSION_MAJOR_NUM : 0;
 }
 // Indicates whether stemming is enabled.
 //
diff --git a/icing/query/advanced_query_parser/query-visitor.cc b/icing/query/advanced_query_parser/query-visitor.cc
index 6178525..8a879f9 100644
--- a/icing/query/advanced_query_parser/query-visitor.cc
+++ b/icing/query/advanced_query_parser/query-visitor.cc
@@ -23,11 +23,13 @@
 #include <string>
 #include <string_view>
 #include <unordered_map>
+#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/annotate.h"
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/absl_ports/str_cat.h"
 #include "icing/absl_ports/str_join.h"
@@ -36,6 +38,7 @@
 #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-filter.h"
+#include "icing/index/iterator/doc-hit-info-iterator-match-score-expression.h"
 #include "icing/index/iterator/doc-hit-info-iterator-none.h"
 #include "icing/index/iterator/doc-hit-info-iterator-not.h"
 #include "icing/index/iterator/doc-hit-info-iterator-or.h"
@@ -43,7 +46,6 @@
 #include "icing/index/iterator/doc-hit-info-iterator-property-in-schema.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/section-restrict-data.h"
 #include "icing/index/property-existence-indexing-handler.h"
 #include "icing/query/advanced_query_parser/abstract-syntax-tree.h"
 #include "icing/query/advanced_query_parser/function.h"
@@ -55,10 +57,12 @@
 #include "icing/query/query-features.h"
 #include "icing/query/query-results.h"
 #include "icing/schema/property-util.h"
-#include "icing/schema/schema-store.h"
 #include "icing/schema/section.h"
+#include "icing/scoring/advanced_scoring/score-expression-util.h"
+#include "icing/scoring/advanced_scoring/score-expression.h"
 #include "icing/tokenization/token.h"
 #include "icing/tokenization/tokenizer.h"
+#include "icing/transform/normalizer.h"
 #include "icing/util/embedding-util.h"
 #include "icing/util/status-macros.h"
 
@@ -295,6 +299,19 @@
   registered_functions_.insert(
       {get_search_string_parameter_function.name(),
        std::move(get_search_string_parameter_function)});
+
+  // DocHitInfoIterator matchScoreExpression(std::string, double, double)
+  auto match_score_expression = [this](std::vector<PendingValue>&& args) {
+    return this->MatchScoreExpressionFunction(std::move(args));
+  };
+  Function match_score_expression_function =
+      Function::Create(DataType::kDocumentIterator, "matchScoreExpression",
+                       {Param(DataType::kString), Param(DataType::kDouble),
+                        Param(DataType::kDouble, Cardinality::kOptional)},
+                       std::move(match_score_expression))
+          .ValueOrDie();
+  registered_functions_.insert({match_score_expression_function.name(),
+                                std::move(match_score_expression_function)});
 }
 
 libtextclassifier3::StatusOr<PendingValue> QueryVisitor::SearchFunction(
@@ -333,9 +350,10 @@
   } else {
     QueryVisitor query_visitor(
         &index_, &numeric_index_, &embedding_index_, &document_store_,
-        &schema_store_, &normalizer_, &tokenizer_, search_spec_,
-        filter_options_, needs_term_frequency_info_,
-        pending_property_restricts_, processing_not_, current_time_ms_);
+        &schema_store_, &normalizer_, &tokenizer_, join_children_fetcher_,
+        search_spec_, filter_options_, needs_term_frequency_info_,
+        &feature_flags_, pending_property_restricts_, processing_not_,
+        current_time_ms_);
     tree_root->Accept(&query_visitor);
     ICING_ASSIGN_OR_RETURN(query_result,
                            std::move(query_visitor).ConsumeResults());
@@ -450,7 +468,8 @@
       std::unique_ptr<DocHitInfoIterator> iterator,
       DocHitInfoIteratorEmbedding::Create(
           &search_spec_.embedding_query_vectors(vector_index), metric_type, low,
-          high, score_map, &embedding_index_));
+          high, score_map, &embedding_index_, &document_store_, &schema_store_,
+          current_time_ms_));
   return PendingValue(std::move(iterator));
 }
 
@@ -471,6 +490,44 @@
   return PendingValue(std::move(iterator));
 }
 
+libtextclassifier3::StatusOr<PendingValue>
+QueryVisitor::MatchScoreExpressionFunction(std::vector<PendingValue>&& args) {
+  const std::string& scoring_expression_str =
+      args.at(0).string_val().ValueOrDie()->term;
+  libtextclassifier3::StatusOr<std::unique_ptr<ScoreExpression>>
+      scoring_expression = score_expression_util::GetScoreExpression(
+          scoring_expression_str,
+          /*default_score=*/-std::numeric_limits<double>::infinity(),
+          SearchSpecProto::EmbeddingQueryMetricType::UNKNOWN, &document_store_,
+          &schema_store_, current_time_ms_, join_children_fetcher_,
+          /*embedding_query_results=*/nullptr, /*section_weights=*/nullptr,
+          /*bm25f_calculator=*/nullptr, /*schema_type_alias_map=*/nullptr,
+          &feature_flags_, &scoring_feature_types_enabled_);
+  if (!scoring_expression.ok()) {
+    return absl_ports::Annotate(
+        scoring_expression.status(),
+        absl_ports::StrCat(
+            "matchScoreExpression: Failed to handle the score expression ",
+            scoring_expression_str));
+  }
+  double low = args.at(1).double_val().ValueOrDie();
+  double high = std::numeric_limits<double>::infinity();
+  if (args.size() == 3) {
+    high = args.at(2).double_val().ValueOrDie();
+  }
+  if (low > high) {
+    return absl_ports::InvalidArgumentError(
+        "The lower bound cannot be greater than the upper bound.");
+  }
+
+  std::unique_ptr<DocHitInfoIterator> iterator =
+      std::make_unique<DocHitInfoIteratorMatchScoreExpression>(
+          document_store_.last_added_document_id(),
+          std::move(scoring_expression).ValueOrDie(), low, high);
+  features_.insert(kMatchScoreExpressionFunctionFeature);
+  return PendingValue(std::move(iterator));
+}
+
 libtextclassifier3::StatusOr<int64_t> QueryVisitor::PopPendingIntValue() {
   if (pending_values_.empty()) {
     return absl_ports::InvalidArgumentError("Unable to retrieve int value.");
@@ -504,7 +561,7 @@
 QueryVisitor::ProduceTextTokenIterators(QueryTerm text_value) {
   ICING_ASSIGN_OR_RETURN(std::unique_ptr<Tokenizer::Iterator> token_itr,
                          tokenizer_.Tokenize(text_value.term));
-  std::string normalized_term;
+  Normalizer::NormalizedTerm normalized_term;
   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
@@ -537,8 +594,9 @@
           raw_token, string_util::FindEscapedToken(raw_text, token.text));
     }
     normalized_term = normalizer_.NormalizeTerm(token.text);
-    QueryTerm term_value{std::move(normalized_term), raw_token,
-                         reached_final_token && text_value.is_prefix_val};
+    bool should_prefix_match = reached_final_token && text_value.is_prefix_val;
+    QueryTerm term_value{std::move(normalized_term.text), raw_token,
+                         should_prefix_match};
     ICING_ASSIGN_OR_RETURN(std::unique_ptr<DocHitInfoIterator> iterator,
                            CreateTermIterator(std::move(term_value)));
     iterators.push_back(std::move(iterator));
diff --git a/icing/query/advanced_query_parser/query-visitor.h b/icing/query/advanced_query_parser/query-visitor.h
index 6089ef4..cd9bedd 100644
--- a/icing/query/advanced_query_parser/query-visitor.h
+++ b/icing/query/advanced_query_parser/query-visitor.h
@@ -20,7 +20,6 @@
 #include <set>
 #include <stack>
 #include <string>
-#include <string_view>
 #include <unordered_map>
 #include <unordered_set>
 #include <utility>
@@ -28,12 +27,14 @@
 
 #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/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/numeric/numeric-index.h"
+#include "icing/join/join-children-fetcher.h"
 #include "icing/query/advanced_query_parser/abstract-syntax-tree.h"
 #include "icing/query/advanced_query_parser/function.h"
 #include "icing/query/advanced_query_parser/pending-value.h"
@@ -44,7 +45,6 @@
 #include "icing/store/document-store.h"
 #include "icing/tokenization/tokenizer.h"
 #include "icing/transform/normalizer.h"
-#include <google/protobuf/repeated_field.h>
 
 namespace icing {
 namespace lib {
@@ -53,21 +53,21 @@
 // the parser.
 class QueryVisitor : public AbstractSyntaxTreeVisitor {
  public:
-  explicit QueryVisitor(Index* index,
-                        const NumericIndex<int64_t>* numeric_index,
-                        const EmbeddingIndex* embedding_index,
-                        const DocumentStore* document_store,
-                        const SchemaStore* schema_store,
-                        const Normalizer* normalizer,
-                        const Tokenizer* tokenizer,
-                        const SearchSpecProto& search_spec,
-                        DocHitInfoIteratorFilter::Options filter_options,
-                        bool needs_term_frequency_info, int64_t current_time_ms)
+  explicit QueryVisitor(
+      Index* index, const NumericIndex<int64_t>* numeric_index,
+      const EmbeddingIndex* embedding_index,
+      const DocumentStore* document_store, const SchemaStore* schema_store,
+      const Normalizer* normalizer, const Tokenizer* tokenizer,
+      const JoinChildrenFetcher* join_children_fetcher,
+      const SearchSpecProto& search_spec,
+      DocHitInfoIteratorFilter::Options filter_options,
+      bool needs_term_frequency_info, const FeatureFlags* feature_flags,
+      int64_t current_time_ms)
       : QueryVisitor(index, numeric_index, embedding_index, document_store,
-                     schema_store, normalizer, tokenizer, search_spec,
-                     filter_options, needs_term_frequency_info,
-                     PendingPropertyRestricts(), /*processing_not=*/false,
-                     current_time_ms) {}
+                     schema_store, normalizer, tokenizer, join_children_fetcher,
+                     search_spec, filter_options, needs_term_frequency_info,
+                     feature_flags, PendingPropertyRestricts(),
+                     /*processing_not=*/false, current_time_ms) {}
 
   void VisitString(const StringNode* node) override;
   void VisitText(const TextNode* node) override;
@@ -111,18 +111,17 @@
     std::vector<std::set<std::string>> pending_property_restricts_;
   };
 
-  explicit QueryVisitor(Index* index,
-                        const NumericIndex<int64_t>* numeric_index,
-                        const EmbeddingIndex* embedding_index,
-                        const DocumentStore* document_store,
-                        const SchemaStore* schema_store,
-                        const Normalizer* normalizer,
-                        const Tokenizer* tokenizer,
-                        const SearchSpecProto& search_spec,
-                        DocHitInfoIteratorFilter::Options filter_options,
-                        bool needs_term_frequency_info,
-                        PendingPropertyRestricts pending_property_restricts,
-                        bool processing_not, int64_t current_time_ms)
+  explicit QueryVisitor(
+      Index* index, const NumericIndex<int64_t>* numeric_index,
+      const EmbeddingIndex* embedding_index,
+      const DocumentStore* document_store, const SchemaStore* schema_store,
+      const Normalizer* normalizer, const Tokenizer* tokenizer,
+      const JoinChildrenFetcher* join_children_fetcher,
+      const SearchSpecProto& search_spec,
+      DocHitInfoIteratorFilter::Options filter_options,
+      bool needs_term_frequency_info, const FeatureFlags* feature_flags,
+      PendingPropertyRestricts pending_property_restricts, bool processing_not,
+      int64_t current_time_ms)
       : index_(*index),
         numeric_index_(*numeric_index),
         embedding_index_(*embedding_index),
@@ -130,9 +129,11 @@
         schema_store_(*schema_store),
         normalizer_(*normalizer),
         tokenizer_(*tokenizer),
+        join_children_fetcher_(join_children_fetcher),
         search_spec_(search_spec),
         filter_options_(std::move(filter_options)),
         needs_term_frequency_info_(needs_term_frequency_info),
+        feature_flags_(*feature_flags),
         pending_property_restricts_(std::move(pending_property_restricts)),
         processing_not_(processing_not),
         expecting_numeric_arg_(false),
@@ -317,6 +318,20 @@
   libtextclassifier3::StatusOr<PendingValue> GetSearchStringParameterFunction(
       std::vector<PendingValue>&& args);
 
+  // Implementation of the matchScoreExpression(scoring_expression, low, high)
+  // custom function.
+  //
+  // high is an optional parameter, which defaults to positive infinity.
+  //
+  // Returns:
+  //   - a Pending Value of type DocHitIterator that returns all documents with
+  //     a score within [low, high] based on the provided scoring expression.
+  //     Documents that cause an evaluation error during scoring are ignored.
+  //   - INVALID_ARGUMENT if low is greater than high.
+  //   - any errors returned by score_expression_util::GetScoreExpression.
+  libtextclassifier3::StatusOr<PendingValue> MatchScoreExpressionFunction(
+      std::vector<PendingValue>&& args);
+
   // Handles a NaryOperatorNode where the operator is HAS (':') and pushes an
   // iterator with the proper section filter applied. If the current property
   // restriction represented by pending_property_restricts and the first child
@@ -359,6 +374,10 @@
   const Normalizer& normalizer_;                // Does not own!
   const Tokenizer& tokenizer_;                  // Does not own!
 
+  // Nullable. A non-null join_children_fetcher_ indicates that this is the
+  // 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_;
 
   DocHitInfoIteratorFilter::Options filter_options_;
@@ -368,6 +387,10 @@
   //  - whether the QueryTermIteratorsMap is populated in the QueryResults.
   bool needs_term_frequency_info_;
 
+  const FeatureFlags& feature_flags_;  // Does not own.
+  // TODO(b/377215223): Pass enabled scoring features from top level.
+  std::unordered_set<ScoringFeatureType> scoring_feature_types_enabled_;
+
   // The stack of property restricts currently being processed by the visitor.
   PendingPropertyRestricts pending_property_restricts_;
   bool processing_not_;
diff --git a/icing/query/advanced_query_parser/query-visitor_test.cc b/icing/query/advanced_query_parser/query-visitor_test.cc
index e07e12e..5a60808 100644
--- a/icing/query/advanced_query_parser/query-visitor_test.cc
+++ b/icing/query/advanced_query_parser/query-visitor_test.cc
@@ -31,6 +31,7 @@
 #include "gtest/gtest.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/portable-file-backed-proto-log.h"
 #include "icing/index/embed/embedding-index.h"
@@ -59,9 +60,9 @@
 #include "icing/store/namespace-id.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/embedding-test-utils.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/jni-test-helpers.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"
@@ -70,6 +71,7 @@
 #include "icing/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
 #include "icing/util/clock.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "icing/util/status-macros.h"
 #include "unicode/uloc.h"
 #include <google/protobuf/repeated_field.h>
@@ -168,6 +170,7 @@
 class QueryVisitorTest : public ::testing::TestWithParam<QueryType> {
  protected:
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     test_dir_ = GetTestTempDir() + "/icing";
     index_dir_ = test_dir_ + "/index";
     numeric_index_dir_ = test_dir_ + "/numeric_index";
@@ -189,24 +192,24 @@
       // setup doesn't do this.
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &clock_, feature_flags_.get()));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(
-            &filesystem_, store_dir_, &clock_, schema_store_.get(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
 
     Index::Options options(index_dir_.c_str(),
@@ -222,7 +225,8 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         embedding_index_,
-        EmbeddingIndex::Create(&filesystem_, embedding_index_dir_));
+        EmbeddingIndex::Create(&filesystem_, embedding_index_dir_, &clock_,
+                               feature_flags_.get()));
 
     ICING_ASSERT_OK_AND_ASSIGN(normalizer_, normalizer_factory::Create(
                                                 /*max_term_byte_size=*/1000));
@@ -253,8 +257,10 @@
     QueryVisitor query_visitor(
         index_.get(), numeric_index_.get(), embedding_index_.get(),
         document_store_.get(), schema_store_.get(), normalizer_.get(),
-        tokenizer_.get(), search_spec, DocHitInfoIteratorFilter::Options(),
-        /*needs_term_frequency_info=*/true, clock_.GetSystemTimeMilliseconds());
+        tokenizer_.get(), /*join_children_fetcher=*/nullptr, search_spec,
+        DocHitInfoIteratorFilter::Options(),
+        /*needs_term_frequency_info=*/true, feature_flags_.get(),
+        clock_.GetSystemTimeMilliseconds());
     root_node->Accept(&query_visitor);
     return std::move(query_visitor).ConsumeResults();
   }
@@ -303,6 +309,7 @@
     }
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   IcingFilesystem icing_filesystem_;
   std::string test_dir_;
@@ -724,8 +731,10 @@
   QueryVisitor query_visitor(
       index_.get(), numeric_index_.get(), embedding_index_.get(),
       document_store_.get(), schema_store_.get(), normalizer_.get(),
-      tokenizer_.get(), search_spec, DocHitInfoIteratorFilter::Options(),
-      /*needs_term_frequency_info_=*/true, clock_.GetSystemTimeMilliseconds());
+      tokenizer_.get(), /*join_children_fetcher=*/nullptr, search_spec,
+      DocHitInfoIteratorFilter::Options(),
+      /*needs_term_frequency_info=*/true, feature_flags_.get(),
+      clock_.GetSystemTimeMilliseconds());
   EXPECT_THAT(std::move(query_visitor).ConsumeResults(),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
@@ -802,12 +811,12 @@
   ICING_ASSERT_OK(editor->BufferKey(1));
   ICING_ASSERT_OK(editor->BufferKey(2));
   ICING_ASSERT_OK(std::move(*editor).IndexAllBufferedKeys());
-  Index::Editor term_editor = index_->Edit(
-      kDocumentId0, kSectionId1, TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(term_editor.BufferTerm("-2"));
-  ICING_ASSERT_OK(term_editor.BufferTerm("-1"));
-  ICING_ASSERT_OK(term_editor.BufferTerm("1"));
-  ICING_ASSERT_OK(term_editor.BufferTerm("2"));
+  Index::Editor term_editor =
+      index_->Edit(kDocumentId0, kSectionId1, /*namespace_id=*/0);
+  ICING_ASSERT_OK(term_editor.BufferTerm("-2", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(term_editor.BufferTerm("-1", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(term_editor.BufferTerm("1", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(term_editor.BufferTerm("2", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(term_editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK(document_store_->Put(
@@ -821,9 +830,9 @@
   editor = numeric_index_->Edit("price", kDocumentId2, kSectionId0);
   ICING_ASSERT_OK(editor->BufferKey(-1));
   ICING_ASSERT_OK(std::move(*editor).IndexAllBufferedKeys());
-  term_editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  term_editor = index_->Edit(kDocumentId2, kSectionId1,
                              /*namespace_id=*/0);
-  ICING_ASSERT_OK(term_editor.BufferTerm("2"));
+  ICING_ASSERT_OK(term_editor.BufferTerm("2", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(term_editor.IndexAllBufferedTerms());
 
   // Translating MINUS chars that are interpreted as NOTs, this query would be
@@ -852,18 +861,18 @@
   // Setup the index with docs 0, 1 and 2 holding the values "foo", "foo" and
   // "bar" respectively.
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = CreateQuery("foo");
@@ -899,18 +908,18 @@
   // Setup the index with docs 0, 1 and 2 holding the values "foo", "foo" and
   // "bar" respectively.
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = CreateQuery("foo");
@@ -920,8 +929,10 @@
   QueryVisitor query_visitor(
       index_.get(), numeric_index_.get(), embedding_index_.get(),
       document_store_.get(), schema_store_.get(), normalizer_.get(),
-      tokenizer_.get(), search_spec, DocHitInfoIteratorFilter::Options(),
-      /*needs_term_frequency_info=*/false, clock_.GetSystemTimeMilliseconds());
+      tokenizer_.get(), /*join_children_fetcher=*/nullptr, search_spec,
+      DocHitInfoIteratorFilter::Options(),
+      /*needs_term_frequency_info=*/false, feature_flags_.get(),
+      clock_.GetSystemTimeMilliseconds());
   root_node->Accept(&query_visitor);
   ICING_ASSERT_OK_AND_ASSIGN(QueryResults query_results,
                              std::move(query_visitor).ConsumeResults());
@@ -951,18 +962,18 @@
   // Setup the index with docs 0, 1 and 2 holding the values "foo", "foo" and
   // "bar" respectively.
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // An EXACT query for 'fo' won't match anything.
@@ -1012,21 +1023,21 @@
   // Setup the index with docs 0, 1 and 2 holding the values ["foo", "ba"],
   // ["foo", "ba"] and ["bar", "fo"] respectively.
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
-  ICING_ASSERT_OK(editor.BufferTerm("ba"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("ba", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
-  ICING_ASSERT_OK(editor.BufferTerm("ba"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("ba", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
-  ICING_ASSERT_OK(editor.BufferTerm("fo"));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("fo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // An EXACT query for `ba?fo` will be lexed into a single TEXT token.
@@ -1068,18 +1079,18 @@
   // Setup the index with docs 0, 1 and 2 holding the values "foo:bar(baz)",
   // "foo:bar(baz)" and "bar:baz(foo)" respectively.
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo:bar(baz)"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm("foo:bar(baz)", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo:bar(baz)"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo:bar(baz)", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar:baz(foo)"));
+  ICING_ASSERT_OK(editor.BufferTerm("bar:baz(foo)", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = CreateQuery("\"foo:bar(baz)\"");
@@ -1105,18 +1116,18 @@
   // Setup the index with docs 0, 1 and 2 holding the values "foo:bar(baz)",
   // "foo:bar(abc)" and "bar:baz(foo)" respectively.
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo:bar(baz)"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm("foo:bar(baz)", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo:bar(abc)"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo:bar(abc)", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar:baz(foo)"));
+  ICING_ASSERT_OK(editor.BufferTerm("bar:baz(foo)", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Query for `"foo:bar("*`. This should match docs 0 and 1.
@@ -1152,18 +1163,18 @@
   // Setup the index with docs 0, 1 and 2 holding the values "foobary",
   // "foobar\" and "foobar"" respectively.
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_EXACT, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm(R"(foobary)"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm(R"(foobary)", TERM_MATCH_EXACT));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_EXACT,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm(R"(foobar\)"));
+  ICING_ASSERT_OK(editor.BufferTerm(R"(foobar\)", TERM_MATCH_EXACT));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_EXACT,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm(R"(foobar")"));
+  ICING_ASSERT_OK(editor.BufferTerm(R"(foobar")", TERM_MATCH_EXACT));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // From the comment above, verbatim_term = `foobar"` and verbatim_query =
@@ -1195,19 +1206,19 @@
   // Setup the index with docs 0, 1 and 2 holding the values "foobary",
   // "foobar\" and "foobar"" respectively.
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_EXACT, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm(R"(foobary)"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm(R"(foobary)", TERM_MATCH_EXACT));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_EXACT,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
   // From the comment above, verbatim_term = `foobar\`.
-  ICING_ASSERT_OK(editor.BufferTerm(R"(foobar\)"));
+  ICING_ASSERT_OK(editor.BufferTerm(R"(foobar\)", TERM_MATCH_EXACT));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_EXACT,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm(R"(foobar")"));
+  ICING_ASSERT_OK(editor.BufferTerm(R"(foobar")", TERM_MATCH_EXACT));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Issue a query for the verbatim token `foobar\`.
@@ -1240,19 +1251,19 @@
   // Setup the index with docs 0, 1 and 2 holding the values "foobary",
   // "foobar\" and "foobar"" respectively.
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_EXACT, /*namespace_id=*/0);
+                                      /*namespace_id=*/0);
   // From the comment above, verbatim_term = `foobary`.
-  ICING_ASSERT_OK(editor.BufferTerm(R"(foobary)"));
+  ICING_ASSERT_OK(editor.BufferTerm(R"(foobary)", TERM_MATCH_EXACT));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_EXACT,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm(R"(foobar\)"));
+  ICING_ASSERT_OK(editor.BufferTerm(R"(foobar\)", TERM_MATCH_EXACT));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_EXACT,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm(R"(foobar\y)"));
+  ICING_ASSERT_OK(editor.BufferTerm(R"(foobar\y)", TERM_MATCH_EXACT));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Issue a query for the verbatim token `foobary`.
@@ -1304,20 +1315,20 @@
   // Setup the index with docs 0, 1 and 2 holding the values "foobar\n",
   // `foobar\` and `foobar\n` respectively.
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_EXACT, /*namespace_id=*/0);
+                                      /*namespace_id=*/0);
   // From the comment above, verbatim_term = `foobar` + '\n'.
-  ICING_ASSERT_OK(editor.BufferTerm("foobar\n"));
+  ICING_ASSERT_OK(editor.BufferTerm("foobar\n", TERM_MATCH_EXACT));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_EXACT,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm(R"(foobar\)"));
+  ICING_ASSERT_OK(editor.BufferTerm(R"(foobar\)", TERM_MATCH_EXACT));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_EXACT,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
   // verbatim_term = `foobar\n`. This is distinct from the term added above.
-  ICING_ASSERT_OK(editor.BufferTerm(R"(foobar\n)"));
+  ICING_ASSERT_OK(editor.BufferTerm(R"(foobar\n)", TERM_MATCH_EXACT));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Issue a query for the verbatim token `foobar` + '\n'.
@@ -1363,20 +1374,20 @@
   // Setup the index with docs 0, 1 and 2 holding the values `foo\"bar\nbaz"`,
   // `foo\\\"bar\\nbaz\"` and `foo\\"bar\\nbaz"` respectively.
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_EXACT, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm(R"(foo\"bar\nbaz")"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm(R"(foo\"bar\nbaz")", TERM_MATCH_EXACT));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_EXACT,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
   // Add the verbatim_term from doc 0 but with all of the escapes left in
-  ICING_ASSERT_OK(editor.BufferTerm(R"(foo\\\"bar\\nbaz\")"));
+  ICING_ASSERT_OK(editor.BufferTerm(R"(foo\\\"bar\\nbaz\")", TERM_MATCH_EXACT));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_EXACT,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
   // Add the verbatim_term from doc 0 but with the escapes for '\' chars left in
-  ICING_ASSERT_OK(editor.BufferTerm(R"(foo\\"bar\\nbaz")"));
+  ICING_ASSERT_OK(editor.BufferTerm(R"(foo\\"bar\\nbaz")", TERM_MATCH_EXACT));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Issue a query for the verbatim token `foo\"bar\nbaz"`.
@@ -1412,22 +1423,22 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*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()));
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  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()));
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = CreateQuery("-foo");
@@ -1457,22 +1468,22 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*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()));
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  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()));
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = CreateQuery("NOT foo");
@@ -1498,26 +1509,26 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
-  ICING_ASSERT_OK(editor.BufferTerm("baz"));
+                                      /*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.BufferTerm("baz", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
-  ICING_ASSERT_OK(editor.BufferTerm("baz"));
+  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()));
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
-  ICING_ASSERT_OK(editor.BufferTerm("baz"));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("baz", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Double negative could be rewritten as `(foo AND NOT bar) baz`
@@ -1547,26 +1558,26 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
-  ICING_ASSERT_OK(editor.BufferTerm("baz"));
+                                      /*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.BufferTerm("baz", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
-  ICING_ASSERT_OK(editor.BufferTerm("baz"));
+  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()));
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
-  ICING_ASSERT_OK(editor.BufferTerm("baz"));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("baz", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Simplifying:
@@ -1596,19 +1607,19 @@
 
 TEST_P(QueryVisitorTest, ImplicitAndTerms) {
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = CreateQuery("foo bar");
@@ -1630,19 +1641,19 @@
 
 TEST_P(QueryVisitorTest, ExplicitAndTerms) {
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = CreateQuery("foo AND bar");
@@ -1664,19 +1675,19 @@
 
 TEST_P(QueryVisitorTest, OrTerms) {
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("fo"));
-  ICING_ASSERT_OK(editor.BufferTerm("ba"));
+  ICING_ASSERT_OK(editor.BufferTerm("fo", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("ba", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = CreateQuery("foo OR bar");
@@ -1698,20 +1709,20 @@
 
 TEST_P(QueryVisitorTest, AndOrTermPrecedence) {
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
-  ICING_ASSERT_OK(editor.BufferTerm("baz"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("baz", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Should be interpreted like `foo (bar OR baz)`
@@ -1780,24 +1791,24 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*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()));
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  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()));
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
-  ICING_ASSERT_OK(editor.BufferTerm("baz"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("baz", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Should be interpreted like `foo ((NOT bar) OR baz)`
@@ -1851,22 +1862,22 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*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()));
-  editor = index_->Edit(kDocumentId1, prop1_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  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()));
-  editor = index_->Edit(kDocumentId2, prop2_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, prop2_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = CreateQuery("foo", /*property_restrict=*/"prop1");
@@ -1918,22 +1929,22 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*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()));
-  editor = index_->Edit(kDocumentId1, prop2_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, prop2_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  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()));
-  editor = index_->Edit(kDocumentId2, prop3_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, prop3_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = R"(search("foo", createList("prop1", "prop2")))";
@@ -2000,22 +2011,22 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*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()));
-  editor = index_->Edit(kDocumentId1, prop1_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  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()));
-  editor = index_->Edit(kDocumentId2, prop2_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, prop2_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = CreateQuery("foo", /*property_restrict=*/"PROP1");
@@ -2061,22 +2072,22 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+                                      /*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()));
-  editor = index_->Edit(kDocumentId1, prop1_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  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()));
-  editor = index_->Edit(kDocumentId2, prop2_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, prop2_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query =
@@ -2120,22 +2131,22 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+                                      /*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()));
-  editor = index_->Edit(kDocumentId1, prop1_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  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()));
-  editor = index_->Edit(kDocumentId2, prop2_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, prop2_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = CreateQuery("(prop1:foo)", /*property_restrict=*/"prop1");
@@ -2190,22 +2201,22 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+                                      /*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()));
-  editor = index_->Edit(kDocumentId1, prop1_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  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()));
-  editor = index_->Edit(kDocumentId2, prop2_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, prop2_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = CreateQuery("(prop2:foo)", /*property_restrict=*/"prop1");
@@ -2256,22 +2267,22 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+                                      /*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()));
-  editor = index_->Edit(kDocumentId1, prop1_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  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()));
-  editor = index_->Edit(kDocumentId2, prop2_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, prop2_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Resulting queries:
@@ -2337,22 +2348,22 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+                                      /*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()));
-  editor = index_->Edit(kDocumentId1, prop1_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  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()));
-  editor = index_->Edit(kDocumentId2, prop2_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, prop2_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Resulting queries:
@@ -2423,35 +2434,35 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("上班"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm("上班", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
-  editor = index_->Edit(kDocumentId0, prop2_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId0, prop2_section_id,
                         /*namespace_id=*/0);
   if (IsCfStringTokenization()) {
-    ICING_ASSERT_OK(editor.BufferTerm("每"));
-    ICING_ASSERT_OK(editor.BufferTerm("夊"));
+    ICING_ASSERT_OK(editor.BufferTerm("每", TERM_MATCH_PREFIX));
+    ICING_ASSERT_OK(editor.BufferTerm("夊", TERM_MATCH_PREFIX));
   } else {
-    ICING_ASSERT_OK(editor.BufferTerm("每天"));
+    ICING_ASSERT_OK(editor.BufferTerm("每天", TERM_MATCH_PREFIX));
   }
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri1").SetSchema("type").Build()));
-  editor = index_->Edit(kDocumentId1, prop1_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("上班"));
+  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()));
-  editor = index_->Edit(kDocumentId2, prop2_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, prop2_section_id,
                         /*namespace_id=*/0);
   if (IsCfStringTokenization()) {
-    ICING_ASSERT_OK(editor.BufferTerm("每"));
-    ICING_ASSERT_OK(editor.BufferTerm("夊"));
+    ICING_ASSERT_OK(editor.BufferTerm("每", TERM_MATCH_PREFIX));
+    ICING_ASSERT_OK(editor.BufferTerm("夊", TERM_MATCH_PREFIX));
   } else {
-    ICING_ASSERT_OK(editor.BufferTerm("每天"));
+    ICING_ASSERT_OK(editor.BufferTerm("每天", TERM_MATCH_PREFIX));
   }
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
@@ -2509,11 +2520,10 @@
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
                              document_store_->Put(doc));
   DocumentId docid0 = put_result0.new_document_id;
-  Index::Editor editor =
-      index_->Edit(docid0, prop0_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val0"));
-  ICING_ASSERT_OK(editor.BufferTerm("val1"));
-  ICING_ASSERT_OK(editor.BufferTerm("val2"));
+  Index::Editor editor = index_->Edit(docid0, prop0_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("val1", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("val2", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // - Doc 1: Contains 'val0', 'val1', 'val2' in 'prop1'. Should match.
@@ -2521,10 +2531,10 @@
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
                              document_store_->Put(doc));
   DocumentId docid1 = put_result1.new_document_id;
-  editor = index_->Edit(docid1, prop1_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val0"));
-  ICING_ASSERT_OK(editor.BufferTerm("val1"));
-  ICING_ASSERT_OK(editor.BufferTerm("val2"));
+  editor = index_->Edit(docid1, prop1_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("val1", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("val2", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // - Doc 2: Contains 'val0', 'val1', 'val2' in 'prop2'. Shouldn't match.
@@ -2532,10 +2542,10 @@
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
                              document_store_->Put(doc));
   DocumentId docid2 = put_result2.new_document_id;
-  editor = index_->Edit(docid2, prop2_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val0"));
-  ICING_ASSERT_OK(editor.BufferTerm("val1"));
-  ICING_ASSERT_OK(editor.BufferTerm("val2"));
+  editor = index_->Edit(docid2, prop2_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("val1", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("val2", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // - Doc 3: Contains 'val0' in 'prop0', 'val1' in 'prop1' etc. Should match.
@@ -2543,14 +2553,14 @@
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
                              document_store_->Put(doc));
   DocumentId docid3 = put_result3.new_document_id;
-  editor = index_->Edit(docid3, prop0_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val0"));
+  editor = index_->Edit(docid3, prop0_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
-  editor = index_->Edit(docid3, prop1_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val1"));
+  editor = index_->Edit(docid3, prop1_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val1", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
-  editor = index_->Edit(docid3, prop2_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val2"));
+  editor = index_->Edit(docid3, prop2_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val2", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // - Doc 4: Contains 'val1' in 'prop0', 'val2' in 'prop1', 'val0' in 'prop2'.
@@ -2559,14 +2569,14 @@
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
                              document_store_->Put(doc));
   DocumentId docid4 = put_result4.new_document_id;
-  editor = index_->Edit(docid4, prop0_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val1"));
+  editor = index_->Edit(docid4, prop0_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val1", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
-  editor = index_->Edit(docid4, prop1_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val2"));
+  editor = index_->Edit(docid4, prop1_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val2", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
-  editor = index_->Edit(docid4, prop1_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val0"));
+  editor = index_->Edit(docid4, prop1_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Now issue a query with 'val1' restricted to 'prop1'. This should match only
@@ -2625,11 +2635,10 @@
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
                              document_store_->Put(doc));
   DocumentId docid0 = put_result0.new_document_id;
-  Index::Editor editor =
-      index_->Edit(docid0, prop0_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val0"));
-  ICING_ASSERT_OK(editor.BufferTerm("val1"));
-  ICING_ASSERT_OK(editor.BufferTerm("val2"));
+  Index::Editor editor = index_->Edit(docid0, prop0_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("val1", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("val2", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // - Doc 1: Contains 'val0', 'val1', 'val2' in 'prop1'. Shouldn't match.
@@ -2637,10 +2646,10 @@
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
                              document_store_->Put(doc));
   DocumentId docid1 = put_result1.new_document_id;
-  editor = index_->Edit(docid1, prop1_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val0"));
-  ICING_ASSERT_OK(editor.BufferTerm("val1"));
-  ICING_ASSERT_OK(editor.BufferTerm("val2"));
+  editor = index_->Edit(docid1, prop1_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("val1", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("val2", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // - Doc 2: Contains 'val0', 'val1', 'val2' in 'prop2'. Should match.
@@ -2648,10 +2657,10 @@
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
                              document_store_->Put(doc));
   DocumentId docid2 = put_result2.new_document_id;
-  editor = index_->Edit(docid2, prop2_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val0"));
-  ICING_ASSERT_OK(editor.BufferTerm("val1"));
-  ICING_ASSERT_OK(editor.BufferTerm("val2"));
+  editor = index_->Edit(docid2, prop2_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("val1", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("val2", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // - Doc 3: Contains 'val0' in 'prop0', 'val1' in 'prop1' etc. Should match.
@@ -2659,14 +2668,14 @@
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
                              document_store_->Put(doc));
   DocumentId docid3 = put_result3.new_document_id;
-  editor = index_->Edit(docid3, prop0_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val0"));
+  editor = index_->Edit(docid3, prop0_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
-  editor = index_->Edit(docid3, prop1_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val1"));
+  editor = index_->Edit(docid3, prop1_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val1", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
-  editor = index_->Edit(docid3, prop2_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val2"));
+  editor = index_->Edit(docid3, prop2_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val2", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // - Doc 4: Contains 'val1' in 'prop0', 'val2' in 'prop1', 'val0' in 'prop2'.
@@ -2675,14 +2684,14 @@
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result4,
                              document_store_->Put(doc));
   DocumentId docid4 = put_result4.new_document_id;
-  editor = index_->Edit(docid4, prop0_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val1"));
+  editor = index_->Edit(docid4, prop0_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val1", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
-  editor = index_->Edit(docid4, prop1_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val2"));
+  editor = index_->Edit(docid4, prop1_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val2", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
-  editor = index_->Edit(docid4, prop1_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("val0"));
+  editor = index_->Edit(docid4, prop1_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("val0", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Now issue a query with 'val1' restricted to 'prop1'. This should match only
@@ -2783,23 +2792,23 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("type").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, prop1_section_id,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+                                      /*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()));
-  editor = index_->Edit(kDocumentId1, prop1_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, prop1_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  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()));
-  editor = index_->Edit(kDocumentId2, prop1_section_id, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, prop1_section_id,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string level_one_query = R"(search("foo", createList("prop1")) bar)";
@@ -2893,65 +2902,64 @@
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
                              document_store_->Put(doc));
   DocumentId docid0 = put_result0.new_document_id;
-  Index::Editor editor =
-      index_->Edit(kDocumentId0, prop0_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  Index::Editor editor = index_->Edit(kDocumentId0, prop0_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri1").Build()));
   DocumentId docid1 = put_result1.new_document_id;
-  editor = index_->Edit(docid1, prop1_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid1, prop1_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri2").Build()));
   DocumentId docid2 = put_result2.new_document_id;
-  editor = index_->Edit(docid2, prop2_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid2, prop2_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri3").Build()));
   DocumentId docid3 = put_result3.new_document_id;
-  editor = index_->Edit(docid3, prop3_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid3, prop3_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result4,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri4").Build()));
   DocumentId docid4 = put_result4.new_document_id;
-  editor = index_->Edit(docid4, prop4_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid4, prop4_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result5,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri5").Build()));
   DocumentId docid5 = put_result5.new_document_id;
-  editor = index_->Edit(docid5, prop5_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid5, prop5_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result6,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri6").Build()));
   DocumentId docid6 = put_result6.new_document_id;
-  editor = index_->Edit(docid6, prop6_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid6, prop6_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result7,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri7").Build()));
   DocumentId docid7 = put_result7.new_document_id;
-  editor = index_->Edit(docid7, prop7_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid7, prop7_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // *If* nested function calls were allowed, then this would simplify as:
@@ -3055,65 +3063,64 @@
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
                              document_store_->Put(doc));
   DocumentId docid0 = put_result0.new_document_id;
-  Index::Editor editor =
-      index_->Edit(kDocumentId0, prop0_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  Index::Editor editor = index_->Edit(kDocumentId0, prop0_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri1").Build()));
   DocumentId docid1 = put_result1.new_document_id;
-  editor = index_->Edit(docid1, prop1_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid1, prop1_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri2").Build()));
   DocumentId docid2 = put_result2.new_document_id;
-  editor = index_->Edit(docid2, prop2_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid2, prop2_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result3,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri3").Build()));
   DocumentId docid3 = put_result3.new_document_id;
-  editor = index_->Edit(docid3, prop3_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid3, prop3_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result4,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri4").Build()));
   DocumentId docid4 = put_result4.new_document_id;
-  editor = index_->Edit(docid4, prop4_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid4, prop4_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result5,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri5").Build()));
   DocumentId docid5 = put_result5.new_document_id;
-  editor = index_->Edit(docid5, prop5_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid5, prop5_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result6,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri6").Build()));
   DocumentId docid6 = put_result6.new_document_id;
-  editor = index_->Edit(docid6, prop6_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid6, prop6_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result7,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri7").Build()));
   DocumentId docid7 = put_result7.new_document_id;
-  editor = index_->Edit(docid7, prop7_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid7, prop7_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // *If* nested function calls were allowed, then this would simplify as:
@@ -3189,26 +3196,25 @@
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
                              document_store_->Put(doc));
   DocumentId docid0 = put_result0.new_document_id;
-  Index::Editor editor =
-      index_->Edit(kDocumentId0, prop0_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  Index::Editor editor = index_->Edit(kDocumentId0, prop0_id, ns_id);
+  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_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri1").Build()));
   DocumentId docid1 = put_result1.new_document_id;
-  editor = index_->Edit(docid1, prop0_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  editor = index_->Edit(docid1, prop0_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri2").Build()));
   DocumentId docid2 = put_result2.new_document_id;
-  editor = index_->Edit(docid2, prop0_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid2, prop0_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = "getSearchStringParameter(0)";
@@ -3310,32 +3316,31 @@
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result0,
                              document_store_->Put(doc));
   DocumentId docid0 = put_result0.new_document_id;
-  Index::Editor editor =
-      index_->Edit(docid0, prop0_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  Index::Editor editor = index_->Edit(docid0, prop0_id, ns_id);
+  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_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri1").Build()));
   DocumentId docid1 = put_result1.new_document_id;
-  editor = index_->Edit(docid1, prop0_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  editor = index_->Edit(docid1, prop0_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
-  editor = index_->Edit(docid1, prop1_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid1, prop1_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result2,
       document_store_->Put(DocumentBuilder(doc).SetUri("uri2").Build()));
   DocumentId docid2 = put_result2.new_document_id;
-  editor = index_->Edit(docid2, prop0_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  editor = index_->Edit(docid2, prop0_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
-  editor = index_->Edit(docid2, prop2_id, TERM_MATCH_PREFIX, ns_id);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  editor = index_->Edit(docid2, prop2_id, ns_id);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = "prop0:getSearchStringParameter(0)";
@@ -3504,8 +3509,8 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("typeWithUrl").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*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.
@@ -3513,17 +3518,17 @@
                                            .SetKey("ns", "uri1")
                                            .SetSchema("typeWithoutUrl")
                                            .Build()));
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // 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()));
-  editor = index_->Edit(kDocumentId2, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = CreateQuery("foo propertyDefined(\"url\")");
@@ -3554,8 +3559,8 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("typeWithUrl").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*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.
@@ -3563,9 +3568,9 @@
                                            .SetKey("ns", "uri1")
                                            .SetSchema("typeWithoutUrl")
                                            .Build()));
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Attempt to query a non-existent property.
@@ -3596,8 +3601,8 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("typeWithUrl").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*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.
@@ -3605,9 +3610,9 @@
                                            .SetKey("ns", "uri1")
                                            .SetSchema("typeWithoutUrl")
                                            .Build()));
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   std::string query = CreateQuery("foo AND NOT propertyDefined(\"url\")");
@@ -3668,28 +3673,30 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("Simple").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId0,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.BufferTerm(
-      absl_ports::StrCat(kPropertyExistenceTokenPrefix, "price").c_str()));
+      absl_ports::StrCat(kPropertyExistenceTokenPrefix, "price").c_str(),
+      TERM_MATCH_PREFIX));
   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()));
-  editor = index_->Edit(kDocumentId1, kSectionId0, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId0,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  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()));
-  editor = index_->Edit(kDocumentId2, kSectionId0, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId2, kSectionId0,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.BufferTerm(
-      absl_ports::StrCat(kPropertyExistenceTokenPrefix, "price").c_str()));
+      absl_ports::StrCat(kPropertyExistenceTokenPrefix, "price").c_str(),
+      TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Test that `foo hasProperty("price")` matches document 0 only.
@@ -3734,18 +3741,19 @@
   ICING_ASSERT_OK(document_store_->Put(
       DocumentBuilder().SetKey("ns", "uri0").SetSchema("Simple").Build()));
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId0,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.BufferTerm(
-      absl_ports::StrCat(kPropertyExistenceTokenPrefix, "price").c_str()));
+      absl_ports::StrCat(kPropertyExistenceTokenPrefix, "price").c_str(),
+      TERM_MATCH_PREFIX));
   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()));
-  editor = index_->Edit(kDocumentId1, kSectionId0, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId0,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Attempt to query a non-existent property.
@@ -3934,15 +3942,38 @@
 }
 
 TEST_F(QueryVisitorTest, SemanticSearchFunctionSimpleLowerBound) {
+  // 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)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build(),
+      /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/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()));
+
   // Index two embedding vectors.
   PropertyProto::VectorProto vector0 =
       CreateVector("my_model", {0.1, 0.2, 0.3});
   PropertyProto::VectorProto vector1 =
       CreateVector("my_model", {-0.1, -0.2, -0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(kSectionId0, kDocumentId0), vector0));
+      BasicHit(kSectionId0, kDocumentId0), vector0, QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(kSectionId0, kDocumentId1), vector1));
+      BasicHit(kSectionId0, kDocumentId1), vector1, QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   // Create an embedding query that has a semantic score of 1 with vector0 and
@@ -4004,15 +4035,38 @@
 }
 
 TEST_F(QueryVisitorTest, SemanticSearchFunctionSimpleUpperBound) {
+  // 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)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build(),
+      /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/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()));
+
   // Index two embedding vectors.
   PropertyProto::VectorProto vector0 =
       CreateVector("my_model", {0.1, 0.2, 0.3});
   PropertyProto::VectorProto vector1 =
       CreateVector("my_model", {-0.1, -0.2, -0.3});
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(kSectionId0, kDocumentId0), vector0));
+      BasicHit(kSectionId0, kDocumentId0), vector0, QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
-      BasicHit(kSectionId0, kDocumentId1), vector1));
+      BasicHit(kSectionId0, kDocumentId1), vector1, QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   // Create an embedding query that has a semantic score of 1 with vector0 and
@@ -4074,10 +4128,31 @@
 }
 
 TEST_F(QueryVisitorTest, SemanticSearchFunctionMetricOverride) {
+  // 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)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build(),
+      /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/false));
+  ICING_ASSERT_OK(document_store_->Put(
+      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));
+      BasicHit(kSectionId0, kDocumentId0), vector, QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   // Create an embedding query that has:
@@ -4144,23 +4219,51 @@
 }
 
 TEST_F(QueryVisitorTest, SemanticSearchFunctionMultipleQueries) {
+  // 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)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("prop3")
+                                        .SetDataTypeVector(
+                                            EMBEDDING_INDEXING_LINEAR_SEARCH)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build(),
+      /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/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()));
+
   // Index 3 embedding vectors for document 0.
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId0),
-      CreateVector("my_model1", {1, -2, -3})));
+      CreateVector("my_model1", {1, -2, -3}), QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId1, kDocumentId0),
-      CreateVector("my_model1", {-1, -2, -3})));
+      CreateVector("my_model1", {-1, -2, -3}), QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId2, kDocumentId0),
-      CreateVector("my_model2", {-1, 2, 3, -4})));
+      CreateVector("my_model2", {-1, 2, 3, -4}), QUANTIZATION_TYPE_NONE));
   // Index 2 embedding vectors for document 1.
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId1),
-      CreateVector("my_model1", {-1, -2, 3})));
+      CreateVector("my_model1", {-1, -2, 3}), QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId1, kDocumentId1),
-      CreateVector("my_model2", {1, -2, 3, -4})));
+      CreateVector("my_model2", {1, -2, 3, -4}), QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   // Create two embedding queries.
@@ -4265,17 +4368,40 @@
 
 TEST_F(QueryVisitorTest,
        SemanticSearchFunctionMultipleQueriesScoresMergedRepeat) {
+  // 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)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build(),
+      /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/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()));
+
   // Index 3 embedding vectors for document 0.
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId0),
-      CreateVector("my_model1", {1, -2, -3})));
+      CreateVector("my_model1", {1, -2, -3}), QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId1, kDocumentId0),
-      CreateVector("my_model1", {-1, -2, -3})));
+      CreateVector("my_model1", {-1, -2, -3}), QUANTIZATION_TYPE_NONE));
   // Index 2 embedding vectors for document 1.
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId1),
-      CreateVector("my_model1", {-1, -2, 3})));
+      CreateVector("my_model1", {-1, -2, 3}), QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   // Create two embedding queries.
@@ -4359,23 +4485,46 @@
 }
 
 TEST_F(QueryVisitorTest, SemanticSearchFunctionHybridQueries) {
+  // 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)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build(),
+      /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/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()));
+
   // Index terms
   Index::Editor editor = index_->Edit(kDocumentId0, kSectionId1,
-                                      TERM_MATCH_PREFIX, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("foo"));
+                                      /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm("foo", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
-  editor = index_->Edit(kDocumentId1, kSectionId1, TERM_MATCH_PREFIX,
+  editor = index_->Edit(kDocumentId1, kSectionId1,
                         /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm("bar"));
+  ICING_ASSERT_OK(editor.BufferTerm("bar", TERM_MATCH_PREFIX));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 
   // Index embedding vectors
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId0),
-      CreateVector("my_model1", {1, -2, -3})));
+      CreateVector("my_model1", {1, -2, -3}), QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId0, kDocumentId1),
-      CreateVector("my_model1", {-1, -2, 3})));
+      CreateVector("my_model1", {-1, -2, 3}), QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   // Create an embedding query with semantic scores:
@@ -4480,16 +4629,16 @@
   // 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})));
+      CreateVector("my_model1", {1, -2, -3}), QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
       BasicHit(kSectionId1, kDocumentId0),
-      CreateVector("my_model1", {-1, -2, 3})));
-  ICING_ASSERT_OK(
-      embedding_index_->BufferEmbedding(BasicHit(kSectionId0, kDocumentId1),
-                                        CreateVector("my_model1", {-1, 2, 3})));
-  ICING_ASSERT_OK(
-      embedding_index_->BufferEmbedding(BasicHit(kSectionId1, kDocumentId1),
-                                        CreateVector("my_model1", {1, 2, -3})));
+      CreateVector("my_model1", {-1, -2, 3}), QUANTIZATION_TYPE_NONE));
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(kSectionId0, kDocumentId1),
+      CreateVector("my_model1", {-1, 2, 3}), QUANTIZATION_TYPE_NONE));
+  ICING_ASSERT_OK(embedding_index_->BufferEmbedding(
+      BasicHit(kSectionId1, kDocumentId1),
+      CreateVector("my_model1", {1, 2, -3}), QUANTIZATION_TYPE_NONE));
   ICING_ASSERT_OK(embedding_index_->CommitBufferToIndex());
 
   // Create an embedding query with semantic scores:
@@ -4534,6 +4683,203 @@
               StatusIs(libtextclassifier3::StatusCode::RESOURCE_EXHAUSTED));
 }
 
+TEST_F(QueryVisitorTest,
+       MatchScoreExpressionFunctionWithInvalidRangeReturnsInvalidArgument) {
+  // The expression is invalid, since low > high.
+  EXPECT_THAT(ProcessQuery("matchScoreExpression(\"1 + 1\", 10, -10)"),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+
+  // Floating point values are also checked.
+  EXPECT_THAT(ProcessQuery("matchScoreExpression(\"1 + 1\", 10.2, 10.1)"),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+
+  // low == high is allowed.
+  EXPECT_THAT(ProcessQuery("matchScoreExpression(\"1 + 1\", 10.1, 10.1)"),
+              IsOk());
+}
+
+TEST_F(QueryVisitorTest, MatchScoreExpressionFunctionSimpleLowerBound) {
+  // Create two documents with different document scores.
+  ICING_ASSERT_OK(schema_store_->SetSchema(
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Simple"))
+          .Build(),
+      /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/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()));
+
+  // Test that `matchScoreExpression("this.documentScore()", 0)` matches
+  // all documents.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      QueryResults query_results,
+      ProcessQuery("matchScoreExpression(\"this.documentScore()\", 0)"));
+  EXPECT_THAT(query_results.features_in_use,
+              UnorderedElementsAre(kMatchScoreExpressionFunctionFeature,
+                                   kListFilterQueryLanguageFeature));
+  EXPECT_THAT(GetDocumentIds(query_results.root_iterator.get()),
+              UnorderedElementsAre(kDocumentId0, kDocumentId1));
+
+  // Test that `matchScoreExpression("this.documentScore()", 2)` matches
+  // document 0 only.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      query_results,
+      ProcessQuery("matchScoreExpression(\"this.documentScore()\", 2)"));
+  EXPECT_THAT(query_results.features_in_use,
+              UnorderedElementsAre(kMatchScoreExpressionFunctionFeature,
+                                   kListFilterQueryLanguageFeature));
+  EXPECT_THAT(GetDocumentIds(query_results.root_iterator.get()),
+              UnorderedElementsAre(kDocumentId0));
+
+  // Test that `matchScoreExpression("this.documentScore()", 5)` matches
+  // no documents.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      query_results,
+      ProcessQuery("matchScoreExpression(\"this.documentScore()\", 5)"));
+  EXPECT_THAT(query_results.features_in_use,
+              UnorderedElementsAre(kMatchScoreExpressionFunctionFeature,
+                                   kListFilterQueryLanguageFeature));
+  EXPECT_THAT(GetDocumentIds(query_results.root_iterator.get()), IsEmpty());
+}
+
+TEST_F(QueryVisitorTest, MatchScoreExpressionFunctionSimpleUpperBound) {
+  // Create two documents with different document scores.
+  ICING_ASSERT_OK(schema_store_->SetSchema(
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Simple"))
+          .Build(),
+      /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/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()));
+
+  // Test that `matchScoreExpression("this.documentScore()", -100, 100)` matches
+  // all documents.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      QueryResults query_results,
+      ProcessQuery(
+          "matchScoreExpression(\"this.documentScore()\", -100, 100)"));
+  EXPECT_THAT(query_results.features_in_use,
+              UnorderedElementsAre(kMatchScoreExpressionFunctionFeature,
+                                   kListFilterQueryLanguageFeature));
+  EXPECT_THAT(GetDocumentIds(query_results.root_iterator.get()),
+              UnorderedElementsAre(kDocumentId0, kDocumentId1));
+
+  // Test that `matchScoreExpression("this.documentScore()", -100, 2)` matches
+  // document 1 only.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      query_results,
+      ProcessQuery("matchScoreExpression(\"this.documentScore()\", -100, 2)"));
+  EXPECT_THAT(query_results.features_in_use,
+              UnorderedElementsAre(kMatchScoreExpressionFunctionFeature,
+                                   kListFilterQueryLanguageFeature));
+  EXPECT_THAT(GetDocumentIds(query_results.root_iterator.get()),
+              UnorderedElementsAre(kDocumentId1));
+
+  // Test that `matchScoreExpression("this.documentScore()", -100, -1)` matches
+  // no documents.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      query_results,
+      ProcessQuery("matchScoreExpression(\"this.documentScore()\", -100, -1)"));
+  EXPECT_THAT(query_results.features_in_use,
+              UnorderedElementsAre(kMatchScoreExpressionFunctionFeature,
+                                   kListFilterQueryLanguageFeature));
+  EXPECT_THAT(GetDocumentIds(query_results.root_iterator.get()), IsEmpty());
+}
+
+TEST_F(QueryVisitorTest, MatchScoreExpressionFunctionComplex) {
+  ICING_ASSERT_OK(schema_store_->SetSchema(
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Simple"))
+          .Build(),
+      /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/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()));
+
+  // Query with a complex score expression:
+  //   `pow(this.creationTimestamp(), 2) + this.documentScore() - 1`.
+  // The score of each document is:
+  // - document 0: 1 * 1 + 4 - 1 = 4
+  // - document 1: 2 * 2 + 2 - 1 = 5
+  // - document 2: 3 * 3 + 0 - 1 = 8
+  // Therefore, filtering with a range of [4.5, 7] will only match document 1.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      QueryResults query_results,
+      ProcessQuery("matchScoreExpression(\"pow(this.creationTimestamp(), 2) + "
+                   "this.documentScore() - 1\", 4.5, 7)"));
+  EXPECT_THAT(query_results.features_in_use,
+              UnorderedElementsAre(kMatchScoreExpressionFunctionFeature,
+                                   kListFilterQueryLanguageFeature));
+  EXPECT_THAT(GetDocumentIds(query_results.root_iterator.get()),
+              UnorderedElementsAre(kDocumentId1));
+}
+
+TEST_F(QueryVisitorTest, MatchScoreExpressionFunctionWithEvaluationErrors) {
+  // Create two documents with different document scores.
+  ICING_ASSERT_OK(schema_store_->SetSchema(
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Simple"))
+          .Build(),
+      /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/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()));
+
+  // Test that documents with evaluation errors will be filtered out.
+  // Specifically, document1 will be filtered out because its document score is
+  // 0, and the expression `1 / 0` is an error.
+  ICING_ASSERT_OK_AND_ASSIGN(
+      QueryResults query_results,
+      ProcessQuery(
+          "matchScoreExpression(\"1 / this.documentScore()\", -10000, 10000)"));
+  EXPECT_THAT(query_results.features_in_use,
+              UnorderedElementsAre(kMatchScoreExpressionFunctionFeature,
+                                   kListFilterQueryLanguageFeature));
+  EXPECT_THAT(GetDocumentIds(query_results.root_iterator.get()),
+              UnorderedElementsAre(kDocumentId0));
+}
+
 INSTANTIATE_TEST_SUITE_P(QueryVisitorTest, QueryVisitorTest,
                          testing::Values(QueryType::kPlain,
                                          QueryType::kSearch));
diff --git a/icing/query/query-features.h b/icing/query/query-features.h
index c84dc7d..10649ba 100644
--- a/icing/query/query-features.h
+++ b/icing/query/query-features.h
@@ -62,10 +62,20 @@
 constexpr Feature kEmbeddingSearchFeatureDeprecated =
     "EMBEDDING_SEARCH";  // Features#EMBEDDING_SEARCH
 
+// This feature relates to the use of the
+// "matchScoreExpression(scoring_expression, low, high)" function.
+//
+// Features#MATCH_SCORE_EXPRESSION_FUNCTION
+constexpr Feature kMatchScoreExpressionFunctionFeature =
+    "MATCH_SCORE_EXPRESSION_FUNCTION";
+
 inline std::unordered_set<Feature> GetQueryFeaturesSet() {
-  return {kNumericSearchFeature,           kVerbatimSearchFeature,
-          kListFilterQueryLanguageFeature, kHasPropertyFunctionFeature,
-          kEmbeddingSearchFeatureDeprecated};
+  return {kNumericSearchFeature,
+          kVerbatimSearchFeature,
+          kListFilterQueryLanguageFeature,
+          kHasPropertyFunctionFeature,
+          kEmbeddingSearchFeatureDeprecated,
+          kMatchScoreExpressionFunctionFeature};
 }
 
 }  // namespace lib
diff --git a/icing/query/query-processor.cc b/icing/query/query-processor.cc
index fcd4f73..c331bb8 100644
--- a/icing/query/query-processor.cc
+++ b/icing/query/query-processor.cc
@@ -23,12 +23,17 @@
 #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/index/embed/embedding-index.h"
 #include "icing/index/index.h"
 #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-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/numeric/numeric-index.h"
+#include "icing/join/join-children-fetcher.h"
 #include "icing/proto/logging.pb.h"
 #include "icing/proto/search.pb.h"
 #include "icing/query/advanced_query_parser/abstract-syntax-tree.h"
@@ -56,7 +61,9 @@
                        const LanguageSegmenter* language_segmenter,
                        const Normalizer* normalizer,
                        const DocumentStore* document_store,
-                       const SchemaStore* schema_store, const Clock* clock) {
+                       const SchemaStore* schema_store,
+                       const JoinChildrenFetcher* join_children_fetcher,
+                       const Clock* clock, const FeatureFlags* feature_flags) {
   ICING_RETURN_ERROR_IF_NULL(index);
   ICING_RETURN_ERROR_IF_NULL(numeric_index);
   ICING_RETURN_ERROR_IF_NULL(embedding_index);
@@ -65,20 +72,21 @@
   ICING_RETURN_ERROR_IF_NULL(document_store);
   ICING_RETURN_ERROR_IF_NULL(schema_store);
   ICING_RETURN_ERROR_IF_NULL(clock);
+  ICING_RETURN_ERROR_IF_NULL(feature_flags);
 
   return std::unique_ptr<QueryProcessor>(new QueryProcessor(
       index, numeric_index, embedding_index, language_segmenter, normalizer,
-      document_store, schema_store, clock));
+      document_store, schema_store, join_children_fetcher, clock,
+      feature_flags));
 }
 
-QueryProcessor::QueryProcessor(Index* index,
-                               const NumericIndex<int64_t>* numeric_index,
-                               const EmbeddingIndex* embedding_index,
-                               const LanguageSegmenter* language_segmenter,
-                               const Normalizer* normalizer,
-                               const DocumentStore* document_store,
-                               const SchemaStore* schema_store,
-                               const Clock* clock)
+QueryProcessor::QueryProcessor(
+    Index* index, const NumericIndex<int64_t>* numeric_index,
+    const EmbeddingIndex* embedding_index,
+    const LanguageSegmenter* language_segmenter, const Normalizer* normalizer,
+    const DocumentStore* document_store, const SchemaStore* schema_store,
+    const JoinChildrenFetcher* join_children_fetcher, const Clock* clock,
+    const FeatureFlags* feature_flags)
     : index_(*index),
       numeric_index_(*numeric_index),
       embedding_index_(*embedding_index),
@@ -86,7 +94,9 @@
       normalizer_(*normalizer),
       document_store_(*document_store),
       schema_store_(*schema_store),
-      clock_(*clock) {}
+      join_children_fetcher_(join_children_fetcher),
+      clock_(*clock),
+      feature_flags_(*feature_flags) {}
 
 libtextclassifier3::StatusOr<QueryResults> QueryProcessor::ParseSearch(
     const SearchSpecProto& search_spec,
@@ -110,6 +120,22 @@
     }
   }
 
+  std::vector<std::unique_ptr<DocHitInfoIterator>> iterators;
+  if (search_spec.document_uri_filters_size() > 0) {
+    ICING_ASSIGN_OR_RETURN(
+        std::unique_ptr<DocHitInfoIteratorByUri> uri_iterator,
+        DocHitInfoIteratorByUri::Create(&document_store_, search_spec));
+    iterators.push_back(std::move(uri_iterator));
+  }
+  if (results.root_iterator != nullptr) {
+    iterators.push_back(std::move(results.root_iterator));
+  }
+  if (iterators.empty()) {
+    iterators.push_back(std::make_unique<DocHitInfoIteratorAllDocumentId>(
+        document_store_.last_added_document_id()));
+  }
+  results.root_iterator = CreateAndIterator(std::move(iterators));
+
   DocHitInfoIteratorFilter::Options options =
       GetFilterOptions(search_spec, document_store_, schema_store_);
   results.root_iterator = std::make_unique<DocHitInfoIteratorFilter>(
@@ -147,10 +173,7 @@
   }
 
   if (tree_root == nullptr) {
-    QueryResults results;
-    results.root_iterator = std::make_unique<DocHitInfoIteratorAllDocumentId>(
-        document_store_.last_added_document_id());
-    return results;
+    return QueryResults{/*root_iterator=*/nullptr};
   }
   ICING_ASSIGN_OR_RETURN(
       std::unique_ptr<Tokenizer> plain_tokenizer,
@@ -164,8 +187,9 @@
   std::unique_ptr<Timer> query_visitor_timer = clock_.GetNewTimer();
   QueryVisitor query_visitor(
       &index_, &numeric_index_, &embedding_index_, &document_store_,
-      &schema_store_, &normalizer_, plain_tokenizer.get(), search_spec,
-      std::move(options), needs_term_frequency_info, current_time_ms);
+      &schema_store_, &normalizer_, plain_tokenizer.get(),
+      join_children_fetcher_, search_spec, std::move(options),
+      needs_term_frequency_info, &feature_flags_, current_time_ms);
   tree_root->Accept(&query_visitor);
   ICING_ASSIGN_OR_RETURN(QueryResults results,
                          std::move(query_visitor).ConsumeResults());
diff --git a/icing/query/query-processor.h b/icing/query/query-processor.h
index d90b5f6..7368ef6 100644
--- a/icing/query/query-processor.h
+++ b/icing/query/query-processor.h
@@ -19,9 +19,11 @@
 #include <memory>
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/feature-flags.h"
 #include "icing/index/embed/embedding-index.h"
 #include "icing/index/index.h"
 #include "icing/index/numeric/numeric-index.h"
+#include "icing/join/join-children-fetcher.h"
 #include "icing/proto/logging.pb.h"
 #include "icing/proto/search.pb.h"
 #include "icing/query/query-results.h"
@@ -51,7 +53,8 @@
       const EmbeddingIndex* embedding_index,
       const LanguageSegmenter* language_segmenter, const Normalizer* normalizer,
       const DocumentStore* document_store, const SchemaStore* schema_store,
-      const Clock* clock);
+      const JoinChildrenFetcher* join_children_fetcher, const Clock* clock,
+      const FeatureFlags* feature_flags);
 
   // Parse the search configurations (including the query, any additional
   // filters, etc.) in the SearchSpecProto into one DocHitInfoIterator.
@@ -74,20 +77,22 @@
       QueryStatsProto::SearchStats* search_stats = nullptr);
 
  private:
-  explicit QueryProcessor(Index* index,
-                          const NumericIndex<int64_t>* numeric_index,
-                          const EmbeddingIndex* embedding_index,
-                          const LanguageSegmenter* language_segmenter,
-                          const Normalizer* normalizer,
-                          const DocumentStore* document_store,
-                          const SchemaStore* schema_store, const Clock* clock);
+  explicit QueryProcessor(
+      Index* index, const NumericIndex<int64_t>* numeric_index,
+      const EmbeddingIndex* embedding_index,
+      const LanguageSegmenter* language_segmenter, const Normalizer* normalizer,
+      const DocumentStore* document_store, const SchemaStore* schema_store,
+      const JoinChildrenFetcher* join_children_fetcher, const Clock* clock,
+      const FeatureFlags* feature_flags);
 
-  // Parse the query into a one DocHitInfoIterator that represents the root of a
-  // query tree in our new Advanced Query Language.
+  // Parse the query into a QueryResults object, which holds a
+  // DocHitInfoIterator that represents the root of a query tree in our new
+  // Advanced Query Language.
   //
   // Returns:
   //   On success,
-  //     - One iterator that represents the entire query
+  //     - A QueryResults instance. If the query is empty, the
+  //       DocHitInfoIterator that it holds will be nullptr.
   //   INVALID_ARGUMENT if query syntax is incorrect and cannot be tokenized
   libtextclassifier3::StatusOr<QueryResults> ParseAdvancedQuery(
       const SearchSpecProto& search_spec,
@@ -118,7 +123,11 @@
   const Normalizer& normalizer_;                 // Does not own.
   const DocumentStore& document_store_;          // Does not own.
   const SchemaStore& schema_store_;              // Does not own.
-  const Clock& clock_;                           // Does not own.
+  // Nullable. A non-null join_children_fetcher_ indicates that this is the
+  // parent query for a join query, in which case child scores are available.
+  const JoinChildrenFetcher* join_children_fetcher_;  // Does not own.
+  const Clock& clock_;                                // Does not own.
+  const FeatureFlags& feature_flags_;                 // Does not own.
 };
 
 }  // namespace lib
diff --git a/icing/query/query-processor_benchmark.cc b/icing/query/query-processor_benchmark.cc
index fd096bf..27120c6 100644
--- a/icing/query/query-processor_benchmark.cc
+++ b/icing/query/query-processor_benchmark.cc
@@ -23,6 +23,7 @@
 #include "third_party/absl/flags/flag.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/portable-file-backed-proto-log.h"
 #include "icing/index/embed/embedding-index.h"
@@ -39,14 +40,15 @@
 #include "icing/store/document-id.h"
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
-#include "icing/testing/icu-data-file-helper.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/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
 #include "icing/util/clock.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "icing/util/logging.h"
 #include "unicode/uloc.h"
 
@@ -87,8 +89,8 @@
                      TermMatchType::Code term_match_type,
                      const std::string& token) {
   Index::Editor editor =
-      index->Edit(document_id, section_id, term_match_type, /*namespace_id=*/0);
-  ICING_ASSERT_OK(editor.BufferTerm(token.c_str()));
+      index->Edit(document_id, section_id, /*namespace_id=*/0);
+  ICING_ASSERT_OK(editor.BufferTerm(token, term_match_type));
   ICING_ASSERT_OK(editor.IndexAllBufferedTerms());
 }
 
@@ -110,23 +112,24 @@
 
 libtextclassifier3::StatusOr<DocumentStore::CreateResult> CreateDocumentStore(
     const Filesystem* filesystem, const std::string& base_dir,
-    const Clock* clock, const SchemaStore* schema_store) {
+    const Clock* clock, const SchemaStore* schema_store,
+    const FeatureFlags& feature_flags) {
   return DocumentStore::Create(
-      filesystem, base_dir, clock, schema_store,
+      filesystem, base_dir, clock, schema_store, &feature_flags,
       /*force_recovery_and_revalidate_documents=*/false,
-      /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-      /*use_persistent_hash_map=*/true,
-      PortableFileBackedProtoLog<DocumentWrapper>::kDeflateCompressionLevel,
+      /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
+      PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
       /*initialize_stats=*/nullptr);
 }
 
 void BM_QueryOneTerm(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   IcingFilesystem icing_filesystem;
   Filesystem filesystem;
   const std::string base_dir = GetTestTempDir() + "/query_processor_benchmark";
@@ -149,9 +152,6 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto numeric_index,
       DummyNumericIndex<int64_t>::Create(filesystem, numeric_index_dir));
-  ICING_ASSERT_OK_AND_ASSIGN(
-      auto embedding_index,
-      EmbeddingIndex::Create(&filesystem, embedding_index_dir));
 
   language_segmenter_factory::SegmenterOptions options(ULOC_US);
   std::unique_ptr<LanguageSegmenter> language_segmenter =
@@ -164,18 +164,23 @@
   Clock clock;
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem, schema_dir, &clock));
+      SchemaStore::Create(&filesystem, schema_dir, &clock, &feature_flags));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/false));
 
   DocumentStore::CreateResult create_result =
       CreateDocumentStore(&filesystem, doc_store_dir, &clock,
-                          schema_store.get())
+                          schema_store.get(), feature_flags)
           .ValueOrDie();
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
+  ICING_ASSERT_OK_AND_ASSIGN(
+      auto embedding_index,
+      EmbeddingIndex::Create(&filesystem, embedding_index_dir, &clock,
+                             &feature_flags));
+
   DocumentId document_id = document_store
                                ->Put(DocumentBuilder()
                                          .SetKey("icing", "type1")
@@ -190,10 +195,11 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QueryProcessor> query_processor,
-      QueryProcessor::Create(index.get(), numeric_index.get(),
-                             embedding_index.get(), language_segmenter.get(),
-                             normalizer.get(), document_store.get(),
-                             schema_store.get(), &clock));
+      QueryProcessor::Create(
+          index.get(), numeric_index.get(), embedding_index.get(),
+          language_segmenter.get(), normalizer.get(), document_store.get(),
+          schema_store.get(), /*join_children_fetcher=*/nullptr, &clock,
+          &feature_flags));
 
   SearchSpecProto search_spec;
   search_spec.set_query(input_string);
@@ -258,10 +264,11 @@
 void BM_QueryFiveTerms(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   IcingFilesystem icing_filesystem;
   Filesystem filesystem;
   const std::string base_dir = GetTestTempDir() + "/query_processor_benchmark";
@@ -284,9 +291,6 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto numeric_index,
       DummyNumericIndex<int64_t>::Create(filesystem, numeric_index_dir));
-  ICING_ASSERT_OK_AND_ASSIGN(
-      auto embedding_index,
-      EmbeddingIndex::Create(&filesystem, embedding_index_dir));
 
   language_segmenter_factory::SegmenterOptions options(ULOC_US);
   std::unique_ptr<LanguageSegmenter> language_segmenter =
@@ -299,18 +303,23 @@
   Clock clock;
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem, schema_dir, &clock));
+      SchemaStore::Create(&filesystem, schema_dir, &clock, &feature_flags));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/false));
 
   DocumentStore::CreateResult create_result =
       CreateDocumentStore(&filesystem, doc_store_dir, &clock,
-                          schema_store.get())
+                          schema_store.get(), feature_flags)
           .ValueOrDie();
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
+  ICING_ASSERT_OK_AND_ASSIGN(
+      auto embedding_index,
+      EmbeddingIndex::Create(&filesystem, embedding_index_dir, &clock,
+                             &feature_flags));
+
   DocumentId document_id = document_store
                                ->Put(DocumentBuilder()
                                          .SetKey("icing", "type1")
@@ -339,10 +348,11 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QueryProcessor> query_processor,
-      QueryProcessor::Create(index.get(), numeric_index.get(),
-                             embedding_index.get(), language_segmenter.get(),
-                             normalizer.get(), document_store.get(),
-                             schema_store.get(), &clock));
+      QueryProcessor::Create(
+          index.get(), numeric_index.get(), embedding_index.get(),
+          language_segmenter.get(), normalizer.get(), document_store.get(),
+          schema_store.get(), /*join_children_fetcher=*/nullptr, &clock,
+          &feature_flags));
 
   const std::string query_string = absl_ports::StrCat(
       input_string_a, " ", input_string_b, " ", input_string_c, " ",
@@ -411,10 +421,11 @@
 void BM_QueryDiacriticTerm(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   IcingFilesystem icing_filesystem;
   Filesystem filesystem;
   const std::string base_dir = GetTestTempDir() + "/query_processor_benchmark";
@@ -437,9 +448,6 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto numeric_index,
       DummyNumericIndex<int64_t>::Create(filesystem, numeric_index_dir));
-  ICING_ASSERT_OK_AND_ASSIGN(
-      auto embedding_index,
-      EmbeddingIndex::Create(&filesystem, embedding_index_dir));
 
   language_segmenter_factory::SegmenterOptions options(ULOC_US);
   std::unique_ptr<LanguageSegmenter> language_segmenter =
@@ -452,18 +460,23 @@
   Clock clock;
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem, schema_dir, &clock));
+      SchemaStore::Create(&filesystem, schema_dir, &clock, &feature_flags));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/false));
 
   DocumentStore::CreateResult create_result =
       CreateDocumentStore(&filesystem, doc_store_dir, &clock,
-                          schema_store.get())
+                          schema_store.get(), feature_flags)
           .ValueOrDie();
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
+  ICING_ASSERT_OK_AND_ASSIGN(
+      auto embedding_index,
+      EmbeddingIndex::Create(&filesystem, embedding_index_dir, &clock,
+                             &feature_flags));
+
   DocumentId document_id = document_store
                                ->Put(DocumentBuilder()
                                          .SetKey("icing", "type1")
@@ -481,10 +494,11 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QueryProcessor> query_processor,
-      QueryProcessor::Create(index.get(), numeric_index.get(),
-                             embedding_index.get(), language_segmenter.get(),
-                             normalizer.get(), document_store.get(),
-                             schema_store.get(), &clock));
+      QueryProcessor::Create(
+          index.get(), numeric_index.get(), embedding_index.get(),
+          language_segmenter.get(), normalizer.get(), document_store.get(),
+          schema_store.get(), /*join_children_fetcher=*/nullptr, &clock,
+          &feature_flags));
 
   SearchSpecProto search_spec;
   search_spec.set_query(input_string);
@@ -549,10 +563,11 @@
 void BM_QueryHiragana(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   IcingFilesystem icing_filesystem;
   Filesystem filesystem;
   const std::string base_dir = GetTestTempDir() + "/query_processor_benchmark";
@@ -575,9 +590,6 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto numeric_index,
       DummyNumericIndex<int64_t>::Create(filesystem, numeric_index_dir));
-  ICING_ASSERT_OK_AND_ASSIGN(
-      auto embedding_index,
-      EmbeddingIndex::Create(&filesystem, embedding_index_dir));
 
   language_segmenter_factory::SegmenterOptions options(ULOC_US);
   std::unique_ptr<LanguageSegmenter> language_segmenter =
@@ -590,18 +602,23 @@
   Clock clock;
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem, schema_dir, &clock));
+      SchemaStore::Create(&filesystem, schema_dir, &clock, &feature_flags));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/false));
 
   DocumentStore::CreateResult create_result =
       CreateDocumentStore(&filesystem, doc_store_dir, &clock,
-                          schema_store.get())
+                          schema_store.get(), feature_flags)
           .ValueOrDie();
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
+  ICING_ASSERT_OK_AND_ASSIGN(
+      auto embedding_index,
+      EmbeddingIndex::Create(&filesystem, embedding_index_dir, &clock,
+                             &feature_flags));
+
   DocumentId document_id = document_store
                                ->Put(DocumentBuilder()
                                          .SetKey("icing", "type1")
@@ -619,10 +636,11 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<QueryProcessor> query_processor,
-      QueryProcessor::Create(index.get(), numeric_index.get(),
-                             embedding_index.get(), language_segmenter.get(),
-                             normalizer.get(), document_store.get(),
-                             schema_store.get(), &clock));
+      QueryProcessor::Create(
+          index.get(), numeric_index.get(), embedding_index.get(),
+          language_segmenter.get(), normalizer.get(), document_store.get(),
+          schema_store.get(), /*join_children_fetcher=*/nullptr, &clock,
+          &feature_flags));
 
   SearchSpecProto search_spec;
   search_spec.set_query(input_string);
diff --git a/icing/query/query-processor_test.cc b/icing/query/query-processor_test.cc
index 536ab5f..b5d3c57 100644
--- a/icing/query/query-processor_test.cc
+++ b/icing/query/query-processor_test.cc
@@ -27,6 +27,7 @@
 #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/index/embed/embedding-index.h"
@@ -53,15 +54,16 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/jni-test-helpers.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/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
 #include "icing/util/clock.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "icing/util/status-macros.h"
 #include "unicode/uloc.h"
 
@@ -78,13 +80,13 @@
 
 libtextclassifier3::StatusOr<DocumentStore::CreateResult> CreateDocumentStore(
     const Filesystem* filesystem, const std::string& base_dir,
-    const Clock* clock, const SchemaStore* schema_store) {
+    const Clock* clock, const SchemaStore* schema_store,
+    const FeatureFlags& feature_flags) {
   return DocumentStore::Create(
-      filesystem, base_dir, clock, schema_store,
+      filesystem, base_dir, clock, schema_store, &feature_flags,
       /*force_recovery_and_revalidate_documents=*/false,
-      /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-      /*use_persistent_hash_map=*/true,
-      PortableFileBackedProtoLog<DocumentWrapper>::kDeflateCompressionLevel,
+      /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
+      PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
       /*initialize_stats=*/nullptr);
 }
 
@@ -99,6 +101,7 @@
         embedding_index_dir_(test_dir_ + "/embedding_index") {}
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
     filesystem_.CreateDirectoryRecursively(index_dir_.c_str());
     filesystem_.CreateDirectoryRecursively(store_dir_.c_str());
@@ -112,17 +115,18 @@
       // setup doesn't do this.
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
+
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
         CreateDocumentStore(&filesystem_, store_dir_, &fake_clock_,
-                            schema_store_.get()));
+                            schema_store_.get(), *feature_flags_));
     document_store_ = std::move(create_result.document_store);
 
     Index::Options options(index_dir_,
@@ -137,7 +141,8 @@
         DummyNumericIndex<int64_t>::Create(filesystem_, numeric_index_dir_));
     ICING_ASSERT_OK_AND_ASSIGN(
         embedding_index_,
-        EmbeddingIndex::Create(&filesystem_, embedding_index_dir_));
+        EmbeddingIndex::Create(&filesystem_, embedding_index_dir_, &fake_clock_,
+                               feature_flags_.get()));
 
     language_segmenter_factory::SegmenterOptions segmenter_options(
         ULOC_US, jni_cache_.get());
@@ -153,15 +158,16 @@
         QueryProcessor::Create(
             index_.get(), numeric_index_.get(), embedding_index_.get(),
             language_segmenter_.get(), normalizer_.get(), document_store_.get(),
-            schema_store_.get(), &fake_clock_));
+            schema_store_.get(), /*join_children_fetcher=*/nullptr,
+            &fake_clock_, feature_flags_.get()));
   }
 
   libtextclassifier3::Status AddTokenToIndex(
       DocumentId document_id, SectionId section_id,
       TermMatchType::Code term_match_type, const std::string& token) {
     Index::Editor editor = index_->Edit(document_id, section_id,
-                                        term_match_type, /*namespace_id=*/0);
-    auto status = editor.BufferTerm(token.c_str());
+                                        /*namespace_id=*/0);
+    auto status = editor.BufferTerm(token, term_match_type);
     return status.ok() ? editor.IndexAllBufferedTerms() : status;
   }
 
@@ -180,6 +186,7 @@
     schema_store_.reset();
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
   }
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   const std::string test_dir_;
   const std::string store_dir_;
@@ -206,52 +213,63 @@
 
 TEST_F(QueryProcessorTest, CreationWithNullPointerShouldFail) {
   EXPECT_THAT(
-      QueryProcessor::Create(/*index=*/nullptr, numeric_index_.get(),
-                             embedding_index_.get(), language_segmenter_.get(),
-                             normalizer_.get(), document_store_.get(),
-                             schema_store_.get(), &fake_clock_),
+      QueryProcessor::Create(
+          /*index=*/nullptr, numeric_index_.get(), embedding_index_.get(),
+          language_segmenter_.get(), normalizer_.get(), document_store_.get(),
+          schema_store_.get(), /*join_children_fetcher=*/nullptr, &fake_clock_,
+          feature_flags_.get()),
       StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
   EXPECT_THAT(
-      QueryProcessor::Create(index_.get(), /*numeric_index_=*/nullptr,
-                             embedding_index_.get(), language_segmenter_.get(),
-                             normalizer_.get(), document_store_.get(),
-                             schema_store_.get(), &fake_clock_),
+      QueryProcessor::Create(
+          index_.get(), /*numeric_index_=*/nullptr, embedding_index_.get(),
+          language_segmenter_.get(), normalizer_.get(), document_store_.get(),
+          schema_store_.get(), /*join_children_fetcher=*/nullptr, &fake_clock_,
+          feature_flags_.get()),
       StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
-  EXPECT_THAT(QueryProcessor::Create(index_.get(), numeric_index_.get(),
-                                     /*embedding_index=*/nullptr,
-                                     language_segmenter_.get(),
-                                     normalizer_.get(), document_store_.get(),
-                                     schema_store_.get(), &fake_clock_),
+  EXPECT_THAT(QueryProcessor::Create(
+                  index_.get(), numeric_index_.get(),
+                  /*embedding_index=*/nullptr, language_segmenter_.get(),
+                  normalizer_.get(), document_store_.get(), schema_store_.get(),
+                  /*join_children_fetcher=*/nullptr, &fake_clock_,
+                  feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
   EXPECT_THAT(QueryProcessor::Create(
                   index_.get(), numeric_index_.get(), embedding_index_.get(),
                   /*language_segmenter=*/nullptr, normalizer_.get(),
-                  document_store_.get(), schema_store_.get(), &fake_clock_),
+                  document_store_.get(), schema_store_.get(),
+                  /*join_children_fetcher=*/nullptr, &fake_clock_,
+                  feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
   EXPECT_THAT(
       QueryProcessor::Create(index_.get(), numeric_index_.get(),
                              embedding_index_.get(), language_segmenter_.get(),
                              /*normalizer=*/nullptr, document_store_.get(),
-                             schema_store_.get(), &fake_clock_),
+                             schema_store_.get(),
+                             /*join_children_fetcher=*/nullptr, &fake_clock_,
+                             feature_flags_.get()),
       StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
-  EXPECT_THAT(
-      QueryProcessor::Create(
-          index_.get(), numeric_index_.get(), embedding_index_.get(),
-          language_segmenter_.get(), normalizer_.get(),
-          /*document_store=*/nullptr, schema_store_.get(), &fake_clock_),
-      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(QueryProcessor::Create(
+                  index_.get(), numeric_index_.get(), embedding_index_.get(),
+                  language_segmenter_.get(), normalizer_.get(),
+                  /*document_store=*/nullptr, schema_store_.get(),
+                  /*join_children_fetcher=*/nullptr, &fake_clock_,
+                  feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
   EXPECT_THAT(
       QueryProcessor::Create(index_.get(), numeric_index_.get(),
                              embedding_index_.get(), language_segmenter_.get(),
                              normalizer_.get(), document_store_.get(),
-                             /*schema_store=*/nullptr, &fake_clock_),
+                             /*schema_store=*/nullptr,
+                             /*join_children_fetcher=*/nullptr, &fake_clock_,
+                             feature_flags_.get()),
       StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
-  EXPECT_THAT(
-      QueryProcessor::Create(index_.get(), numeric_index_.get(),
-                             embedding_index_.get(), language_segmenter_.get(),
-                             normalizer_.get(), document_store_.get(),
-                             schema_store_.get(), /*clock=*/nullptr),
-      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(QueryProcessor::Create(
+                  index_.get(), numeric_index_.get(), embedding_index_.get(),
+                  language_segmenter_.get(), normalizer_.get(),
+                  document_store_.get(), schema_store_.get(),
+                  /*join_children_fetcher=*/nullptr, /*clock=*/nullptr,
+                  feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 }
 
 TEST_F(QueryProcessorTest, EmptyGroupMatchAllDocuments) {
@@ -2945,7 +2963,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, store_dir_, &fake_clock,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   document_store_ = std::move(create_result.document_store);
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -2972,7 +2990,9 @@
       QueryProcessor::Create(index_.get(), numeric_index_.get(),
                              embedding_index_.get(), language_segmenter_.get(),
                              normalizer_.get(), document_store_.get(),
-                             schema_store_.get(), &fake_clock_));
+                             schema_store_.get(),
+                             /*join_children_fetcher=*/nullptr, &fake_clock_,
+                             feature_flags_.get()));
 
   SearchSpecProto search_spec;
   search_spec.set_query("hello");
@@ -3008,7 +3028,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, store_dir_, &fake_clock_local,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   document_store_ = std::move(create_result.document_store);
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -3035,7 +3055,9 @@
       QueryProcessor::Create(index_.get(), numeric_index_.get(),
                              embedding_index_.get(), language_segmenter_.get(),
                              normalizer_.get(), document_store_.get(),
-                             schema_store_.get(), &fake_clock_));
+                             schema_store_.get(),
+                             /*join_children_fetcher=*/nullptr, &fake_clock_,
+                             feature_flags_.get()));
 
   SearchSpecProto search_spec;
   search_spec.set_query("hello");
@@ -3387,6 +3409,40 @@
               Eq(kSearchStatsLatencyMs));
 }
 
+TEST_F(QueryProcessorTest, UriFiltersIsNotTheRightMostNode) {
+  SchemaProto schema = SchemaBuilder()
+                           .AddType(SchemaTypeConfigBuilder().SetType("email"))
+                           .Build();
+  ASSERT_THAT(schema_store_->SetSchema(
+                  schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/false),
+              IsOk());
+  ICING_ASSERT_OK(document_store_->Put(DocumentBuilder()
+                                           .SetKey("namespace", "uri1")
+                                           .SetSchema("email")
+                                           .Build()));
+
+  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");
+
+  QueryStatsProto::SearchStats search_stats;
+  ICING_ASSERT_OK_AND_ASSIGN(
+      QueryResults results,
+      query_processor_->ParseSearch(
+          search_spec, ScoringSpecProto::RankingStrategy::NONE,
+          fake_clock_.GetSystemTimeMilliseconds(), &search_stats));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocHitInfoIterator::TrimmedNode trimmed_node,
+      std::move(*results.root_iterator).TrimRightMostNode());
+  EXPECT_THAT(trimmed_node.iterator_->ToString(), Eq("uri_iterator"));
+}
+
 }  // namespace
 
 }  // namespace lib
diff --git a/icing/query/suggestion-processor.cc b/icing/query/suggestion-processor.cc
index ba80093..cebd205 100644
--- a/icing/query/suggestion-processor.cc
+++ b/icing/query/suggestion-processor.cc
@@ -26,6 +26,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/index/embed/embedding-index.h"
 #include "icing/index/index.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
@@ -56,8 +57,8 @@
                             const LanguageSegmenter* language_segmenter,
                             const Normalizer* normalizer,
                             const DocumentStore* document_store,
-                            const SchemaStore* schema_store,
-                            const Clock* clock) {
+                            const SchemaStore* schema_store, const Clock* clock,
+                            const FeatureFlags* feature_flags) {
   ICING_RETURN_ERROR_IF_NULL(index);
   ICING_RETURN_ERROR_IF_NULL(numeric_index);
   ICING_RETURN_ERROR_IF_NULL(embedding_index);
@@ -66,10 +67,11 @@
   ICING_RETURN_ERROR_IF_NULL(document_store);
   ICING_RETURN_ERROR_IF_NULL(schema_store);
   ICING_RETURN_ERROR_IF_NULL(clock);
+  ICING_RETURN_ERROR_IF_NULL(feature_flags);
 
   return std::unique_ptr<SuggestionProcessor>(new SuggestionProcessor(
       index, numeric_index, embedding_index, language_segmenter, normalizer,
-      document_store, schema_store, clock));
+      document_store, schema_store, clock, feature_flags));
 }
 
 libtextclassifier3::StatusOr<
@@ -251,9 +253,10 @@
 
   ICING_ASSIGN_OR_RETURN(
       std::unique_ptr<QueryProcessor> query_processor,
-      QueryProcessor::Create(&index_, &numeric_index_, &embedding_index_,
-                             &language_segmenter_, &normalizer_,
-                             &document_store_, &schema_store_, &clock_));
+      QueryProcessor::Create(
+          &index_, &numeric_index_, &embedding_index_, &language_segmenter_,
+          &normalizer_, &document_store_, &schema_store_,
+          /*join_children_fetcher=*/nullptr, &clock_, &feature_flags_));
 
   SearchSpecProto search_spec;
   search_spec.set_query(suggestion_spec.prefix());
@@ -343,7 +346,7 @@
     const EmbeddingIndex* embedding_index,
     const LanguageSegmenter* language_segmenter, const Normalizer* normalizer,
     const DocumentStore* document_store, const SchemaStore* schema_store,
-    const Clock* clock)
+    const Clock* clock, const FeatureFlags* feature_flags)
     : index_(*index),
       numeric_index_(*numeric_index),
       embedding_index_(*embedding_index),
@@ -351,7 +354,8 @@
       normalizer_(*normalizer),
       document_store_(*document_store),
       schema_store_(*schema_store),
-      clock_(*clock) {}
+      clock_(*clock),
+      feature_flags_(*feature_flags) {}
 
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/query/suggestion-processor.h b/icing/query/suggestion-processor.h
index cf393b4..5b21611 100644
--- a/icing/query/suggestion-processor.h
+++ b/icing/query/suggestion-processor.h
@@ -20,9 +20,11 @@
 #include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/feature-flags.h"
 #include "icing/index/embed/embedding-index.h"
 #include "icing/index/index.h"
 #include "icing/index/numeric/numeric-index.h"
+#include "icing/index/term-metadata.h"
 #include "icing/proto/search.pb.h"
 #include "icing/schema/schema-store.h"
 #include "icing/store/document-store.h"
@@ -50,7 +52,8 @@
          const EmbeddingIndex* embedding_index,
          const LanguageSegmenter* language_segmenter,
          const Normalizer* normalizer, const DocumentStore* document_store,
-         const SchemaStore* schema_store, const Clock* clock);
+         const SchemaStore* schema_store, const Clock* clock,
+         const FeatureFlags* feature_flags);
 
   // Query suggestions based on the given SuggestionSpecProto.
   //
@@ -62,14 +65,12 @@
       const SuggestionSpecProto& suggestion_spec, int64_t current_time_ms);
 
  private:
-  explicit SuggestionProcessor(Index* index,
-                               const NumericIndex<int64_t>* numeric_index,
-                               const EmbeddingIndex* embedding_index,
-                               const LanguageSegmenter* language_segmenter,
-                               const Normalizer* normalizer,
-                               const DocumentStore* document_store,
-                               const SchemaStore* schema_store,
-                               const Clock* clock);
+  explicit SuggestionProcessor(
+      Index* index, const NumericIndex<int64_t>* numeric_index,
+      const EmbeddingIndex* embedding_index,
+      const LanguageSegmenter* language_segmenter, const Normalizer* normalizer,
+      const DocumentStore* document_store, const SchemaStore* schema_store,
+      const Clock* clock, const FeatureFlags* feature_flags);
 
   // Not const because we could modify/sort the TermMetaData buffer in the lite
   // index.
@@ -81,6 +82,7 @@
   const DocumentStore& document_store_;          // Does not own.
   const SchemaStore& schema_store_;              // Does not own.
   const Clock& clock_;                           // Does not own.
+  const FeatureFlags& feature_flags_;            // Does not own.
 };
 
 }  // namespace lib
diff --git a/icing/query/suggestion-processor_test.cc b/icing/query/suggestion-processor_test.cc
index 4347a69..ba6be86 100644
--- a/icing/query/suggestion-processor_test.cc
+++ b/icing/query/suggestion-processor_test.cc
@@ -24,6 +24,7 @@
 #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/index/embed/embedding-index.h"
@@ -42,14 +43,15 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/jni-test-helpers.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/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
 namespace icing {
@@ -82,6 +84,7 @@
         embedding_index_dir_(test_dir_ + "/embedding_index") {}
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
     filesystem_.CreateDirectoryRecursively(index_dir_.c_str());
     filesystem_.CreateDirectoryRecursively(store_dir_.c_str());
@@ -95,24 +98,24 @@
       // setup doesn't do this.
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(
-            &filesystem_, store_dir_, &fake_clock_, schema_store_.get(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
 
     Index::Options options(index_dir_,
@@ -127,7 +130,8 @@
         DummyNumericIndex<int64_t>::Create(filesystem_, numeric_index_dir_));
     ICING_ASSERT_OK_AND_ASSIGN(
         embedding_index_,
-        EmbeddingIndex::Create(&filesystem_, embedding_index_dir_));
+        EmbeddingIndex::Create(&filesystem_, embedding_index_dir_, &fake_clock_,
+                               feature_flags_.get()));
 
     language_segmenter_factory::SegmenterOptions segmenter_options(
         ULOC_US, jni_cache_.get());
@@ -143,15 +147,15 @@
         SuggestionProcessor::Create(
             index_.get(), numeric_index_.get(), embedding_index_.get(),
             language_segmenter_.get(), normalizer_.get(), document_store_.get(),
-            schema_store_.get(), &fake_clock_));
+            schema_store_.get(), &fake_clock_, feature_flags_.get()));
   }
 
   libtextclassifier3::Status AddTokenToIndex(
       DocumentId document_id, SectionId section_id,
       TermMatchType::Code term_match_type, const std::string& token) {
     Index::Editor editor = index_->Edit(document_id, section_id,
-                                        term_match_type, /*namespace_id=*/0);
-    auto status = editor.BufferTerm(token.c_str());
+                                        /*namespace_id=*/0);
+    auto status = editor.BufferTerm(token, term_match_type);
     return status.ok() ? editor.IndexAllBufferedTerms() : status;
   }
 
@@ -161,6 +165,7 @@
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   const std::string test_dir_;
   const std::string store_dir_;
diff --git a/icing/result/result-adjustment-info_test.cc b/icing/result/result-adjustment-info_test.cc
index cbce557..fd0551e 100644
--- a/icing/result/result-adjustment-info_test.cc
+++ b/icing/result/result-adjustment-info_test.cc
@@ -14,11 +14,13 @@
 
 #include "icing/result/result-adjustment-info.h"
 
+#include <memory>
 #include <string>
 #include <unordered_set>
 #include <vector>
 
 #include "gtest/gtest.h"
+#include "icing/feature-flags.h"
 #include "icing/proto/scoring.pb.h"
 #include "icing/proto/search.pb.h"
 #include "icing/proto/term.pb.h"
@@ -28,6 +30,7 @@
 #include "icing/schema/schema-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
+#include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 
 namespace icing {
@@ -49,9 +52,10 @@
   }
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, test_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           &fake_clock_, feature_flags_.get()));
 
     SchemaProto schema =
         SchemaBuilder()
@@ -68,6 +72,7 @@
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   const Filesystem filesystem_;
   const std::string test_dir_;
   std::unique_ptr<SchemaStore> schema_store_;
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 215035e..0d59e41 100644
--- a/icing/result/result-retriever-v2_group-result-limiter_test.cc
+++ b/icing/result/result-retriever-v2_group-result-limiter_test.cc
@@ -17,6 +17,8 @@
 
 #include "gtest/gtest.h"
 #include "icing/document-builder.h"
+#include "icing/feature-flags.h"
+#include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/portable/equals-proto.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document.pb.h"
@@ -30,14 +32,16 @@
 #include "icing/scoring/priority-queue-scored-document-hits-ranker.h"
 #include "icing/scoring/scored-document-hit.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/icu-data-file-helper.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/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
 namespace icing {
@@ -62,10 +66,11 @@
   }
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
     language_segmenter_factory::SegmenterOptions options(ULOC_US);
@@ -74,8 +79,8 @@
         language_segmenter_factory::Create(std::move(options)));
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, test_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     ICING_ASSERT_OK_AND_ASSIGN(normalizer_, normalizer_factory::Create(
                                                 /*max_term_byte_size=*/10000));
 
@@ -89,14 +94,14 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(
-            &filesystem_, test_dir_, &fake_clock_, schema_store_.get(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
   }
 
@@ -104,6 +109,7 @@
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   const Filesystem filesystem_;
   const std::string test_dir_;
   std::unique_ptr<LanguageSegmenter> language_segmenter_;
diff --git a/icing/result/result-retriever-v2_projection_test.cc b/icing/result/result-retriever-v2_projection_test.cc
index f846ef6..1ac0681 100644
--- a/icing/result/result-retriever-v2_projection_test.cc
+++ b/icing/result/result-retriever-v2_projection_test.cc
@@ -18,6 +18,8 @@
 
 #include "gtest/gtest.h"
 #include "icing/document-builder.h"
+#include "icing/feature-flags.h"
+#include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/portable/equals-proto.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document.pb.h"
@@ -35,14 +37,16 @@
 #include "icing/scoring/priority-queue-scored-document-hits-ranker.h"
 #include "icing/scoring/scored-document-hit.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/icu-data-file-helper.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/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
 namespace icing {
@@ -60,10 +64,11 @@
   }
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
     language_segmenter_factory::SegmenterOptions options(ULOC_US);
@@ -72,8 +77,8 @@
         language_segmenter_factory::Create(std::move(options)));
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, test_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     ICING_ASSERT_OK_AND_ASSIGN(normalizer_, normalizer_factory::Create(
                                                 /*max_term_byte_size=*/10000));
 
@@ -184,14 +189,14 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(
-            &filesystem_, test_dir_, &fake_clock_, schema_store_.get(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
   }
 
@@ -218,6 +223,7 @@
     return kInvalidSectionId;
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   const Filesystem filesystem_;
   const std::string test_dir_;
   std::unique_ptr<LanguageSegmenter> language_segmenter_;
diff --git a/icing/result/result-retriever-v2_snippet_test.cc b/icing/result/result-retriever-v2_snippet_test.cc
index 7bad0d8..114637e 100644
--- a/icing/result/result-retriever-v2_snippet_test.cc
+++ b/icing/result/result-retriever-v2_snippet_test.cc
@@ -19,6 +19,8 @@
 
 #include "gtest/gtest.h"
 #include "icing/document-builder.h"
+#include "icing/feature-flags.h"
+#include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/portable/equals-proto.h"
 #include "icing/portable/platform.h"
 #include "icing/proto/document.pb.h"
@@ -35,14 +37,16 @@
 #include "icing/scoring/priority-queue-scored-document-hits-ranker.h"
 #include "icing/scoring/scored-document-hit.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/icu-data-file-helper.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/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "icing/util/snippet-helpers.h"
 #include "unicode/uloc.h"
 
@@ -64,10 +68,11 @@
   }
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
     language_segmenter_factory::SegmenterOptions options(ULOC_US);
@@ -76,8 +81,8 @@
         language_segmenter_factory::Create(std::move(options)));
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, test_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     ICING_ASSERT_OK_AND_ASSIGN(normalizer_, normalizer_factory::Create(
                                                 /*max_term_byte_size=*/10000));
 
@@ -109,14 +114,14 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(
-            &filesystem_, test_dir_, &fake_clock_, schema_store_.get(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
   }
 
@@ -143,6 +148,7 @@
     return kInvalidSectionId;
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   const Filesystem filesystem_;
   const std::string test_dir_;
   std::unique_ptr<LanguageSegmenter> language_segmenter_;
diff --git a/icing/result/result-retriever-v2_test.cc b/icing/result/result-retriever-v2_test.cc
index 4ae4e4c..77e6fa2 100644
--- a/icing/result/result-retriever-v2_test.cc
+++ b/icing/result/result-retriever-v2_test.cc
@@ -29,6 +29,7 @@
 #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/mock-filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
@@ -50,14 +51,15 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.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/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
 #include "icing/util/clock.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
 namespace icing {
@@ -97,10 +99,11 @@
   }
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
     language_segmenter_factory::SegmenterOptions options(ULOC_US);
@@ -109,8 +112,8 @@
         language_segmenter_factory::Create(std::move(options)));
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, test_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     ICING_ASSERT_OK_AND_ASSIGN(normalizer_, normalizer_factory::Create(
                                                 /*max_term_byte_size=*/10000));
 
@@ -179,6 +182,7 @@
     return kInvalidSectionId;
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   const Filesystem filesystem_;
   const std::string test_dir_;
   std::unique_ptr<LanguageSegmenter> language_segmenter_;
@@ -216,13 +220,13 @@
 
 libtextclassifier3::StatusOr<DocumentStore::CreateResult> CreateDocumentStore(
     const Filesystem* filesystem, const std::string& base_dir,
-    const Clock* clock, const SchemaStore* schema_store) {
+    const Clock* clock, const SchemaStore* schema_store,
+    const FeatureFlags& feature_flags) {
   return DocumentStore::Create(
-      filesystem, base_dir, clock, schema_store,
+      filesystem, base_dir, clock, schema_store, &feature_flags,
       /*force_recovery_and_revalidate_documents=*/false,
-      /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-      /*use_persistent_hash_map=*/true,
-      PortableFileBackedProtoLog<DocumentWrapper>::kDeflateCompressionLevel,
+      /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
+      PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
       /*initialize_stats=*/nullptr);
 }
 
@@ -235,7 +239,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
@@ -257,7 +261,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
@@ -355,7 +359,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
@@ -430,7 +434,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
@@ -592,7 +596,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&mock_filesystem, test_dir_, &fake_clock_,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
@@ -642,7 +646,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
@@ -741,7 +745,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
@@ -854,7 +858,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
@@ -916,7 +920,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
@@ -980,7 +984,7 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem_, test_dir_, &fake_clock_,
-                          schema_store_.get()));
+                          schema_store_.get(), *feature_flags_));
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
 
diff --git a/icing/result/result-state-manager_test.cc b/icing/result/result-state-manager_test.cc
index b06eb94..e998c99 100644
--- a/icing/result/result-state-manager_test.cc
+++ b/icing/result/result-state-manager_test.cc
@@ -14,10 +14,14 @@
 
 #include "icing/result/result-state-manager.h"
 
+#include <memory>
+
 #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/equals-proto.h"
 #include "icing/result/page-result.h"
 #include "icing/result/result-adjustment-info.h"
@@ -28,13 +32,14 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.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/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
 #include "icing/util/clock.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
 namespace icing {
@@ -79,10 +84,11 @@
   }
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
 
@@ -94,8 +100,8 @@
         language_segmenter_factory::Create(std::move(options)));
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, test_dir_, clock_.get()));
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           clock_.get(), feature_flags_.get()));
     SchemaProto schema;
     schema.add_types()->set_schema_type("Document");
     ICING_ASSERT_OK(schema_store_->SetSchema(
@@ -107,14 +113,14 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult result,
-        DocumentStore::Create(
-            &filesystem_, test_dir_, clock_.get(), schema_store_.get(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     document_store_ = std::move(result.document_store);
 
     ICING_ASSERT_OK_AND_ASSIGN(
@@ -173,6 +179,7 @@
   }
 
  private:
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   const std::string test_dir_;
   std::unique_ptr<FakeClock> clock_;
diff --git a/icing/result/result-state-manager_thread-safety_test.cc b/icing/result/result-state-manager_thread-safety_test.cc
index dd0f6f1..33cde3f 100644
--- a/icing/result/result-state-manager_thread-safety_test.cc
+++ b/icing/result/result-state-manager_thread-safety_test.cc
@@ -13,13 +13,16 @@
 // limitations under the License.
 
 #include <algorithm>
+#include <memory>
 #include <optional>
 #include <thread>  // NOLINT
 
 #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/equals-proto.h"
 #include "icing/result/page-result.h"
 #include "icing/result/result-retriever-v2.h"
@@ -29,13 +32,14 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.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/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
 #include "icing/util/clock.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
 namespace icing {
@@ -72,10 +76,11 @@
   }
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
 
@@ -87,8 +92,8 @@
         language_segmenter_factory::Create(std::move(options)));
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, test_dir_, clock_.get()));
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           clock_.get(), feature_flags_.get()));
     SchemaProto schema;
     schema.add_types()->set_schema_type("Document");
     ICING_ASSERT_OK(schema_store_->SetSchema(
@@ -100,14 +105,14 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult result,
-        DocumentStore::Create(
-            &filesystem_, test_dir_, clock_.get(), schema_store_.get(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     document_store_ = std::move(result.document_store);
 
     ICING_ASSERT_OK_AND_ASSIGN(
@@ -121,6 +126,7 @@
     clock_.reset();
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   const std::string test_dir_;
   std::unique_ptr<FakeClock> clock_;
diff --git a/icing/result/result-state-v2_test.cc b/icing/result/result-state-v2_test.cc
index 9915dbf..bb2031c 100644
--- a/icing/result/result-state-v2_test.cc
+++ b/icing/result/result-state-v2_test.cc
@@ -25,6 +25,7 @@
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "icing/absl_ports/mutex.h"
+#include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/proto/document.pb.h"
@@ -38,6 +39,7 @@
 #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"
 
@@ -61,11 +63,13 @@
 class ResultStateV2Test : public ::testing::Test {
  protected:
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     schema_store_base_dir_ = GetTestTempDir() + "/schema_store";
     filesystem_.CreateDirectoryRecursively(schema_store_base_dir_.c_str());
+
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_base_dir_, &clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_base_dir_,
+                                           &clock_, feature_flags_.get()));
     SchemaProto schema;
     schema.add_types()->set_schema_type("Document");
     ICING_ASSERT_OK(schema_store_->SetSchema(
@@ -76,14 +80,14 @@
     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(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     document_store_ = std::move(result.document_store);
 
     num_total_hits_ = 0;
@@ -110,6 +114,7 @@
   const std::atomic<int>& num_total_hits() const { return num_total_hits_; }
 
  private:
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   std::string doc_store_base_dir_;
   std::string schema_store_base_dir_;
diff --git a/icing/result/snippet-retriever.cc b/icing/result/snippet-retriever.cc
index 9b10c33..53bff43 100644
--- a/icing/result/snippet-retriever.cc
+++ b/icing/result/snippet-retriever.cc
@@ -61,7 +61,8 @@
 
 // Returns a string of the normalized text of the input Token. Normalization
 // is applied based on the Token's type.
-std::string NormalizeToken(const Normalizer& normalizer, const Token& token) {
+Normalizer::NormalizedTerm NormalizeToken(const Normalizer& normalizer,
+                                          const Token& token) {
   switch (token.type) {
     case Token::Type::RFC822_NAME:
       [[fallthrough]];
@@ -106,7 +107,7 @@
     case Token::Type::REGULAR:
       return normalizer.NormalizeTerm(token.text);
     case Token::Type::VERBATIM:
-      return std::string(token.text);
+      return {std::string(token.text)};
     case Token::Type::QUERY_EXCLUSION:
       [[fallthrough]];
     case Token::Type::QUERY_LEFT_PARENTHESES:
@@ -120,7 +121,7 @@
     case Token::Type::INVALID:
       ICING_LOG(WARNING) << "Unable to normalize token of type: "
                          << static_cast<int>(token.type);
-      return std::string(token.text);
+      return {std::string(token.text)};
   }
 }
 
@@ -227,10 +228,11 @@
         normalizer_(normalizer) {}
 
   CharacterIterator Matches(Token token) const override {
-    std::string s = NormalizeToken(normalizer_, token);
-    auto itr = unrestricted_query_terms_.find(s);
+    Normalizer::NormalizedTerm normalized_term =
+        NormalizeToken(normalizer_, token);
+    auto itr = unrestricted_query_terms_.find(normalized_term.text);
     if (itr == unrestricted_query_terms_.end()) {
-      itr = restricted_query_terms_.find(s);
+      itr = restricted_query_terms_.find(normalized_term.text);
     }
     if (itr != unrestricted_query_terms_.end() &&
         itr != restricted_query_terms_.end()) {
@@ -257,16 +259,19 @@
         normalizer_(normalizer) {}
 
   CharacterIterator Matches(Token token) const override {
-    std::string s = NormalizeToken(normalizer_, token);
+    Normalizer::NormalizedTerm normalized_term =
+        NormalizeToken(normalizer_, token);
     for (const std::string& query_term : unrestricted_query_terms_) {
-      if (query_term.length() <= s.length() &&
-          s.compare(0, query_term.length(), query_term) == 0) {
+      if (query_term.length() <= normalized_term.text.length() &&
+          normalized_term.text.compare(0, query_term.length(), query_term) ==
+              0) {
         return FindMatchEnd(normalizer_, token, query_term);
       }
     }
     for (const std::string& query_term : restricted_query_terms_) {
-      if (query_term.length() <= s.length() &&
-          s.compare(0, query_term.length(), query_term) == 0) {
+      if (query_term.length() <= normalized_term.text.length() &&
+          normalized_term.text.compare(0, query_term.length(), query_term) ==
+              0) {
         return FindMatchEnd(normalizer_, token, query_term);
       }
     }
diff --git a/icing/result/snippet-retriever_benchmark.cc b/icing/result/snippet-retriever_benchmark.cc
index e574325..b4d10da 100644
--- a/icing/result/snippet-retriever_benchmark.cc
+++ b/icing/result/snippet-retriever_benchmark.cc
@@ -16,6 +16,7 @@
 #include "gmock/gmock.h"
 #include "third_party/absl/flags/flag.h"
 #include "icing/document-builder.h"
+#include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/proto/search.pb.h"
@@ -24,13 +25,14 @@
 #include "icing/schema/schema-store.h"
 #include "icing/schema/section.h"
 #include "icing/testing/common-matchers.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/random-string.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/transform/normalizer-factory.h"
 #include "icing/util/clock.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "icing/util/logging.h"
 #include "unicode/uloc.h"
 
@@ -72,10 +74,11 @@
 void BM_SnippetOneProperty(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   const std::string base_dir = GetTestTempDir() + "/query_processor_benchmark";
   const std::string schema_dir = base_dir + "/schema";
   Filesystem filesystem;
@@ -103,7 +106,7 @@
   Clock clock;
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem, schema_dir, &clock));
+      SchemaStore::Create(&filesystem, schema_dir, &clock, &feature_flags));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/false));
@@ -201,10 +204,11 @@
 void BM_SnippetRfcOneProperty(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   const std::string base_dir = GetTestTempDir() + "/query_processor_benchmark";
   const std::string schema_dir = base_dir + "/schema";
   Filesystem filesystem;
@@ -232,7 +236,7 @@
   Clock clock;
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem, schema_dir, &clock));
+      SchemaStore::Create(&filesystem, schema_dir, &clock, &feature_flags));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/false));
diff --git a/icing/result/snippet-retriever_test.cc b/icing/result/snippet-retriever_test.cc
index 8d81b43..8c07e7a 100644
--- a/icing/result/snippet-retriever_test.cc
+++ b/icing/result/snippet-retriever_test.cc
@@ -21,6 +21,7 @@
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "icing/document-builder.h"
+#include "icing/feature-flags.h"
 #include "icing/file/mock-filesystem.h"
 #include "icing/portable/equals-proto.h"
 #include "icing/portable/platform.h"
@@ -36,15 +37,16 @@
 #include "icing/store/key-mapper.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/jni-test-helpers.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/transform/map/map-normalizer.h"
 #include "icing/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "icing/util/snippet-helpers.h"
 #include "unicode/uloc.h"
 
@@ -76,13 +78,14 @@
 class SnippetRetrieverTest : public testing::Test {
  protected:
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     test_dir_ = GetTestTempDir() + "/icing";
     filesystem_.CreateDirectoryRecursively(test_dir_.c_str());
 
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
 
@@ -95,8 +98,8 @@
 
     // Setup the schema
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, test_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     SchemaProto schema =
         SchemaBuilder()
             .AddType(
@@ -136,6 +139,7 @@
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   FakeClock fake_clock_;
   std::unique_ptr<SchemaStore> schema_store_;
diff --git a/icing/schema-builder.h b/icing/schema-builder.h
index 9387440..ab14d7e 100644
--- a/icing/schema-builder.h
+++ b/icing/schema-builder.h
@@ -63,6 +63,11 @@
 constexpr EmbeddingIndexingConfig::EmbeddingIndexingType::Code
     EMBEDDING_INDEXING_LINEAR_SEARCH =
         EmbeddingIndexingConfig::EmbeddingIndexingType::LINEAR_SEARCH;
+constexpr EmbeddingIndexingConfig::QuantizationType::Code
+    QUANTIZATION_TYPE_NONE = EmbeddingIndexingConfig::QuantizationType::NONE;
+constexpr EmbeddingIndexingConfig::QuantizationType::Code
+    QUANTIZATION_TYPE_QUANTIZE_8_BIT =
+        EmbeddingIndexingConfig::QuantizationType::QUANTIZE_8_BIT;
 
 constexpr PropertyConfigProto::DataType::Code TYPE_UNKNOWN =
     PropertyConfigProto::DataType::UNKNOWN;
@@ -88,6 +93,19 @@
 constexpr JoinableConfig::ValueType::Code JOINABLE_VALUE_TYPE_QUALIFIED_ID =
     JoinableConfig::ValueType::QUALIFIED_ID;
 
+constexpr JoinableConfig::DeletePropagationType::Code
+    DELETE_PROPAGATION_TYPE_NONE = JoinableConfig::DeletePropagationType::NONE;
+constexpr JoinableConfig::DeletePropagationType::Code
+    DELETE_PROPAGATION_TYPE_PROPAGATE_FROM =
+        JoinableConfig::DeletePropagationType::PROPAGATE_FROM;
+
+constexpr PropertyConfigProto::ScorableType::Code SCORABLE_TYPE_ENABLED =
+    PropertyConfigProto::ScorableType::ENABLED;
+constexpr PropertyConfigProto::ScorableType::Code SCORABLE_TYPE_DISABLED =
+    PropertyConfigProto::ScorableType::DISABLED;
+constexpr PropertyConfigProto::ScorableType::Code SCORABLE_TYPE_UNKNOWN =
+    PropertyConfigProto::ScorableType::UNKNOWN;
+
 class PropertyConfigBuilder {
  public:
   PropertyConfigBuilder() = default;
@@ -116,12 +134,12 @@
 
   PropertyConfigBuilder& SetDataTypeJoinableString(
       JoinableConfig::ValueType::Code join_value_type,
-      TermMatchType::Code match_type = TERM_MATCH_UNKNOWN,
-      StringIndexingConfig::TokenizerType::Code tokenizer = TOKENIZER_NONE) {
+      JoinableConfig::DeletePropagationType::Code delete_propagation_type =
+          DELETE_PROPAGATION_TYPE_NONE) {
     property_.set_data_type(PropertyConfigProto::DataType::STRING);
     property_.mutable_joinable_config()->set_value_type(join_value_type);
-    property_.mutable_string_indexing_config()->set_term_match_type(match_type);
-    property_.mutable_string_indexing_config()->set_tokenizer_type(tokenizer);
+    property_.mutable_joinable_config()->set_delete_propagation_type(
+        delete_propagation_type);
     return *this;
   }
 
@@ -160,17 +178,24 @@
 
   PropertyConfigBuilder& SetDataTypeVector(
       EmbeddingIndexingConfig::EmbeddingIndexingType::Code
-          embedding_indexing_type) {
+          embedding_indexing_type,
+      EmbeddingIndexingConfig::QuantizationType::Code quantization_type =
+          EmbeddingIndexingConfig::QuantizationType::NONE) {
     property_.set_data_type(PropertyConfigProto::DataType::VECTOR);
-    property_.mutable_embedding_indexing_config()->set_embedding_indexing_type(
+    EmbeddingIndexingConfig* embedding_indexing_config =
+        property_.mutable_embedding_indexing_config();
+    embedding_indexing_config->set_embedding_indexing_type(
         embedding_indexing_type);
+    embedding_indexing_config->set_quantization_type(quantization_type);
     return *this;
   }
 
   PropertyConfigBuilder& SetJoinable(
-      JoinableConfig::ValueType::Code join_value_type, bool propagate_delete) {
+      JoinableConfig::ValueType::Code join_value_type,
+      JoinableConfig::DeletePropagationType::Code delete_propagation_type) {
     property_.mutable_joinable_config()->set_value_type(join_value_type);
-    property_.mutable_joinable_config()->set_propagate_delete(propagate_delete);
+    property_.mutable_joinable_config()->set_delete_propagation_type(
+        delete_propagation_type);
     return *this;
   }
 
@@ -185,6 +210,12 @@
     return *this;
   }
 
+  PropertyConfigBuilder& SetScorableType(
+      PropertyConfigProto::ScorableType::Code scorable_type) {
+    property_.set_scorable_type(scorable_type);
+    return *this;
+  }
+
   PropertyConfigProto Build() const { return std::move(property_); }
 
  private:
@@ -217,6 +248,11 @@
     return *this;
   }
 
+  SchemaTypeConfigBuilder& SetDatabase(std::string database) {
+    type_config_.set_database(std::move(database));
+    return *this;
+  }
+
   SchemaTypeConfigBuilder& AddProperty(PropertyConfigProto property) {
     *type_config_.add_properties() = std::move(property);
     return *this;
diff --git a/icing/schema/joinable-property-manager-builder_test.cc b/icing/schema/joinable-property-manager-builder_test.cc
index ac48faa..f573ac7 100644
--- a/icing/schema/joinable-property-manager-builder_test.cc
+++ b/icing/schema/joinable-property-manager-builder_test.cc
@@ -62,21 +62,21 @@
       PropertyConfigBuilder()
           .SetDataType(TYPE_STRING)
           .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                       /*propagate_delete=*/true)
+                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
           .SetCardinality(CARDINALITY_OPTIONAL)
           .Build();
   PropertyConfigProto prop_bar =
       PropertyConfigBuilder()
           .SetDataType(TYPE_STRING)
           .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                       /*propagate_delete=*/false)
+                       DELETE_PROPAGATION_TYPE_NONE)
           .SetCardinality(CARDINALITY_OPTIONAL)
           .Build();
   PropertyConfigProto prop_baz =
       PropertyConfigBuilder()
           .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
           .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                       /*propagate_delete=*/true)
+                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
           .SetCardinality(CARDINALITY_REQUIRED)
           .Build();
 
@@ -123,7 +123,7 @@
         PropertyConfigBuilder()
             .SetDataType(TYPE_STRING)
             .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                         /*propagate_delete=*/true)
+                         DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
             .SetCardinality(CARDINALITY_REQUIRED)
             .Build();
     ICING_ASSERT_OK(builder.ProcessSchemaTypePropertyConfig(
@@ -136,7 +136,7 @@
       PropertyConfigBuilder()
           .SetDataType(TYPE_STRING)
           .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                       /*propagate_delete=*/true)
+                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
           .SetCardinality(CARDINALITY_REQUIRED)
           .Build();
   EXPECT_THAT(builder.ProcessSchemaTypePropertyConfig(
@@ -159,7 +159,7 @@
       PropertyConfigBuilder()
           .SetDataType(TYPE_STRING)
           .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                       /*propagate_delete=*/true)
+                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
           .SetCardinality(CARDINALITY_REQUIRED)
           .Build();
 
@@ -189,7 +189,7 @@
       PropertyConfigBuilder()
           .SetDataType(TYPE_STRING)
           .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                       /*propagate_delete=*/true)
+                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
           .SetCardinality(CARDINALITY_REQUIRED)
           .Build();
 
@@ -216,56 +216,56 @@
           .SetName("int1")
           .SetDataType(TYPE_INT64)
           .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                       /*propagate_delete=*/true)
+                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
           .SetCardinality(CARDINALITY_REQUIRED)
           .Build(),
       PropertyConfigBuilder()
           .SetName("int2")
           .SetDataType(TYPE_INT64)
           .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                       /*propagate_delete=*/false)
+                       DELETE_PROPAGATION_TYPE_NONE)
           .SetCardinality(CARDINALITY_REQUIRED)
           .Build(),
       PropertyConfigBuilder()
           .SetName("double1")
           .SetDataType(TYPE_DOUBLE)
           .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                       /*propagate_delete=*/true)
+                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
           .SetCardinality(CARDINALITY_REQUIRED)
           .Build(),
       PropertyConfigBuilder()
           .SetName("double2")
           .SetDataType(TYPE_DOUBLE)
           .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                       /*propagate_delete=*/false)
+                       DELETE_PROPAGATION_TYPE_NONE)
           .SetCardinality(CARDINALITY_REQUIRED)
           .Build(),
       PropertyConfigBuilder()
           .SetName("boolean1")
           .SetDataType(TYPE_BOOLEAN)
           .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                       /*propagate_delete=*/true)
+                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
           .SetCardinality(CARDINALITY_REQUIRED)
           .Build(),
       PropertyConfigBuilder()
           .SetName("boolean2")
           .SetDataType(TYPE_BOOLEAN)
           .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                       /*propagate_delete=*/false)
+                       DELETE_PROPAGATION_TYPE_NONE)
           .SetCardinality(CARDINALITY_REQUIRED)
           .Build(),
       PropertyConfigBuilder()
           .SetName("bytes1")
           .SetDataType(TYPE_BYTES)
           .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                       /*propagate_delete=*/true)
+                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
           .SetCardinality(CARDINALITY_REQUIRED)
           .Build(),
       PropertyConfigBuilder()
           .SetName("bytes2")
           .SetDataType(TYPE_BYTES)
           .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                       /*propagate_delete=*/false)
+                       DELETE_PROPAGATION_TYPE_NONE)
           .SetCardinality(CARDINALITY_REQUIRED)
           .Build(),
       PropertyConfigBuilder()
@@ -273,7 +273,7 @@
           .SetDataTypeDocument(/*schema_type=*/"SchemaTypeTwo",
                                /*index_nested_properties=*/true)
           .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                       /*propagate_delete=*/true)
+                       DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
           .SetCardinality(CARDINALITY_REQUIRED)
           .Build(),
       PropertyConfigBuilder()
@@ -281,7 +281,7 @@
           .SetDataTypeDocument(/*schema_type=*/"SchemaTypeTwo",
                                /*index_nested_properties=*/true)
           .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                       /*propagate_delete=*/false)
+                       DELETE_PROPAGATION_TYPE_NONE)
           .SetCardinality(CARDINALITY_REQUIRED)
           .Build()};
 
@@ -335,14 +335,14 @@
                         .SetName("property")
                         .SetDataType(TYPE_STRING)
                         .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                                     /*propagate_delete=*/true)
+                                     DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
                         .SetCardinality(CARDINALITY_OPTIONAL)
                         .Build(),
                     PropertyConfigBuilder()
                         .SetName("property")
                         .SetDataType(TYPE_STRING)
                         .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                                     /*propagate_delete=*/false)
+                                     DELETE_PROPAGATION_TYPE_NONE)
                         .SetCardinality(CARDINALITY_OPTIONAL)
                         .Build(),
                     // Indexable string can be configured joinable as well. For
@@ -351,14 +351,14 @@
                         .SetName("property")
                         .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
                         .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                                     /*propagate_delete=*/true)
+                                     DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
                         .SetCardinality(CARDINALITY_OPTIONAL)
                         .Build(),
                     PropertyConfigBuilder()
                         .SetName("property")
                         .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
                         .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                                     /*propagate_delete=*/false)
+                                     DELETE_PROPAGATION_TYPE_NONE)
                         .SetCardinality(CARDINALITY_OPTIONAL)
                         .Build()));
 
diff --git a/icing/schema/joinable-property-manager.cc b/icing/schema/joinable-property-manager.cc
index 1606abb..e51e143 100644
--- a/icing/schema/joinable-property-manager.cc
+++ b/icing/schema/joinable-property-manager.cc
@@ -14,7 +14,7 @@
 
 #include "icing/schema/joinable-property-manager.h"
 
-#include <memory>
+#include <cstdint>
 #include <string>
 #include <string_view>
 #include <utility>
@@ -42,7 +42,8 @@
         metadata_list_wrapper,
     std::string&& concatenated_path,
     PropertyConfigProto::DataType::Code data_type,
-    JoinableConfig::ValueType::Code value_type) {
+    JoinableConfig::ValueType::Code value_type,
+    JoinableConfig::DeletePropagationType::Code delete_propagation_type) {
   // Validates next joinable property id, makes sure that joinable property id
   // is the same as the list index so that we could find any joinable property
   // metadata by id in O(1) later.
@@ -58,7 +59,8 @@
 
   // Creates joinable property metadata
   metadata_list_wrapper->metadata_list.push_back(JoinablePropertyMetadata(
-      new_id, data_type, value_type, std::move(concatenated_path)));
+      new_id, data_type, value_type, delete_propagation_type,
+      std::move(concatenated_path)));
   metadata_list_wrapper->property_path_to_id_map.insert(
       {metadata_list_wrapper->metadata_list.back().path, new_id});
   return libtextclassifier3::Status::OK;
@@ -101,7 +103,8 @@
         ICING_RETURN_IF_ERROR(AppendNewJoinablePropertyMetadata(
             &joinable_property_metadata_cache_[schema_type_id],
             std::move(property_path), PropertyConfigProto::DataType::STRING,
-            JoinableConfig::ValueType::QUALIFIED_ID));
+            JoinableConfig::ValueType::QUALIFIED_ID,
+            property_config.joinable_config().delete_propagation_type()));
       }
       break;
     }
diff --git a/icing/schema/joinable-property-manager_test.cc b/icing/schema/joinable-property-manager_test.cc
index ceaaa18..f3a6a2d 100644
--- a/icing/schema/joinable-property-manager_test.cc
+++ b/icing/schema/joinable-property-manager_test.cc
@@ -72,7 +72,8 @@
   return PropertyConfigBuilder()
       .SetName(kPropertySenderQualifiedId)
       .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
-      .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID, /*propagate_delete=*/true)
+      .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                   DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
       .SetCardinality(CARDINALITY_OPTIONAL)
       .Build();
 }
@@ -81,7 +82,8 @@
   return PropertyConfigBuilder()
       .SetName(kPropertyReceiverQualifiedId)
       .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
-      .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID, /*propagate_delete=*/true)
+      .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                   DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
       .SetCardinality(CARDINALITY_OPTIONAL)
       .Build();
 }
@@ -90,7 +92,8 @@
   return PropertyConfigBuilder()
       .SetName(kPropertyGroupQualifiedId)
       .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
-      .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID, /*propagate_delete=*/false)
+      .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                   DELETE_PROPAGATION_TYPE_NONE)
       .SetCardinality(CARDINALITY_OPTIONAL)
       .Build();
 }
diff --git a/icing/schema/joinable-property.h b/icing/schema/joinable-property.h
index 057bb74..416d870 100644
--- a/icing/schema/joinable-property.h
+++ b/icing/schema/joinable-property.h
@@ -70,14 +70,30 @@
   //   non-string DataType.
   JoinableConfig::ValueType::Code value_type;
 
+  // How to propagate the deletion between the document and the referenced
+  // joinable document.
+  //
+  // JoinableConfig::DeletePropagationType::NONE:
+  //   No propagation.
+  //
+  // JoinableConfig::DeletePropagationType::PROPAGATE_FROM:
+  //   Delete the document when the referenced (parent) document is deleted.
+  //
+  // This is only applicable to joinable properties with value type
+  // JoinableConfig::ValueType::QUALIFIED_ID.
+  JoinableConfig::DeletePropagationType::Code delete_propagation_type;
+
   explicit JoinablePropertyMetadata(
       JoinablePropertyId id_in,
       PropertyConfigProto::DataType::Code data_type_in,
-      JoinableConfig::ValueType::Code value_type_in, std::string&& path_in)
+      JoinableConfig::ValueType::Code value_type_in,
+      JoinableConfig::DeletePropagationType::Code delete_propagation_type_in,
+      std::string&& path_in)
       : path(std::move(path_in)),
         id(id_in),
         data_type(data_type_in),
-        value_type(value_type_in) {}
+        value_type(value_type_in),
+        delete_propagation_type(delete_propagation_type_in) {}
 
   JoinablePropertyMetadata(const JoinablePropertyMetadata& other) = default;
   JoinablePropertyMetadata& operator=(const JoinablePropertyMetadata& other) =
@@ -89,7 +105,8 @@
 
   bool operator==(const JoinablePropertyMetadata& rhs) const {
     return path == rhs.path && id == rhs.id && data_type == rhs.data_type &&
-           value_type == rhs.value_type;
+           value_type == rhs.value_type &&
+           delete_propagation_type == rhs.delete_propagation_type;
   }
 };
 
@@ -115,6 +132,10 @@
   JoinableConfig::ValueType::Code value_type() const {
     return metadata.value_type;
   }
+
+  JoinableConfig::DeletePropagationType::Code delete_propagation_type() const {
+    return metadata.delete_propagation_type;
+  }
 };
 
 // Groups of different type joinable properties. Callers can access joinable
diff --git a/icing/schema/property-util.cc b/icing/schema/property-util.cc
index 4c07306..e8eeec7 100644
--- a/icing/schema/property-util.cc
+++ b/icing/schema/property-util.cc
@@ -134,6 +134,20 @@
 }
 
 template <>
+libtextclassifier3::StatusOr<std::vector<double>> ExtractPropertyValues<double>(
+    const PropertyProto& property) {
+  return std::vector<double>(property.double_values().begin(),
+                             property.double_values().end());
+}
+
+template <>
+libtextclassifier3::StatusOr<std::vector<bool>> ExtractPropertyValues<bool>(
+    const PropertyProto& property) {
+  return std::vector<bool>(property.boolean_values().begin(),
+                           property.boolean_values().end());
+}
+
+template <>
 libtextclassifier3::StatusOr<std::vector<PropertyProto::VectorProto>>
 ExtractPropertyValues<PropertyProto::VectorProto>(
     const PropertyProto& property) {
diff --git a/icing/schema/property-util.h b/icing/schema/property-util.h
index 55e4f20..d07489e 100644
--- a/icing/schema/property-util.h
+++ b/icing/schema/property-util.h
@@ -167,6 +167,14 @@
 ExtractPropertyValues<int64_t>(const PropertyProto& property);
 
 template <>
+libtextclassifier3::StatusOr<std::vector<double>> ExtractPropertyValues<double>(
+    const PropertyProto& property);
+
+template <>
+libtextclassifier3::StatusOr<std::vector<bool>> ExtractPropertyValues<bool>(
+    const PropertyProto& property);
+
+template <>
 libtextclassifier3::StatusOr<std::vector<PropertyProto::VectorProto>>
 ExtractPropertyValues<PropertyProto::VectorProto>(
     const PropertyProto& property);
diff --git a/icing/schema/property-util_test.cc b/icing/schema/property-util_test.cc
index 1bfd5cd..c401bd3 100644
--- a/icing/schema/property-util_test.cc
+++ b/icing/schema/property-util_test.cc
@@ -33,9 +33,12 @@
 namespace {
 
 using ::icing::lib::portable_equals_proto::EqualsProto;
+using ::testing::DoubleNear;
 using ::testing::ElementsAre;
 using ::testing::IsEmpty;
 
+constexpr double kEps = 0.0000001;  // 1e-7
+
 static constexpr std::string_view kTypeTest = "Test";
 static constexpr std::string_view kPropertySingleString = "singleString";
 static constexpr std::string_view kPropertyRepeatedString = "repeatedString";
@@ -88,6 +91,25 @@
               IsOkAndHolds(ElementsAre(123, -456, 0)));
 }
 
+TEST(PropertyUtilTest, ExtractPropertyValuesTypeDouble) {
+  PropertyProto property;
+  property.mutable_double_values()->Add(3.5);
+  property.mutable_double_values()->Add(-2.0);
+
+  EXPECT_THAT(
+      property_util::ExtractPropertyValues<double>(property),
+      IsOkAndHolds(ElementsAre(DoubleNear(3.5, kEps), DoubleNear(-2.0, kEps))));
+}
+
+TEST(PropertyUtilTest, ExtractPropertyValuesTypeBoolean) {
+  PropertyProto property;
+  property.mutable_boolean_values()->Add(true);
+  property.mutable_boolean_values()->Add(false);
+
+  EXPECT_THAT(property_util::ExtractPropertyValues<bool>(property),
+              IsOkAndHolds(ElementsAre(true, false)));
+}
+
 TEST(PropertyUtilTest, ExtractPropertyValuesTypeVector) {
   PropertyProto::VectorProto vector1;
   vector1.set_model_signature("my_model1");
@@ -112,12 +134,12 @@
 
 TEST(PropertyUtilTest, ExtractPropertyValuesTypeBlobHandle) {
   PropertyProto::BlobHandleProto blob_handle1;
-  blob_handle1.set_label("label1");
+  blob_handle1.set_namespace_("namespace1");
   std::vector<unsigned char> bytes1(32);
   std::string digestString1 = std::string(bytes1.begin(), bytes1.end());
   blob_handle1.set_digest(digestString1);
   PropertyProto::BlobHandleProto blob_handle2;
-  blob_handle2.set_label("label2");
+  blob_handle2.set_namespace_("namespace2");
   std::vector<unsigned char> bytes2(32);
   std::string digestString2 = std::string(bytes2.begin(), bytes2.end());
   blob_handle2.set_digest(digestString2);
diff --git a/icing/schema/schema-store.cc b/icing/schema/schema-store.cc
index 2b04adb..aaa9f84 100644
--- a/icing/schema/schema-store.cc
+++ b/icing/schema/schema-store.cc
@@ -15,9 +15,11 @@
 #include "icing/schema/schema-store.h"
 
 #include <cinttypes>
+#include <cstddef>
 #include <cstdint>
 #include <limits>
 #include <memory>
+#include <optional>
 #include <string>
 #include <string_view>
 #include <unordered_map>
@@ -29,6 +31,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/destructible-directory.h"
 #include "icing/file/file-backed-proto.h"
 #include "icing/file/filesystem.h"
@@ -46,6 +49,7 @@
 #include "icing/schema/schema-property-iterator.h"
 #include "icing/schema/schema-type-manager.h"
 #include "icing/schema/schema-util.h"
+#include "icing/schema/scorable_property_manager.h"
 #include "icing/schema/section.h"
 #include "icing/store/document-filter-data.h"
 #include "icing/store/dynamic-trie-key-mapper.h"
@@ -64,6 +68,12 @@
 constexpr char kOverlaySchemaFilename[] = "overlay_schema.pb";
 constexpr char kSchemaTypeMapperFilename[] = "schema_type_mapper";
 
+// This should be kept consistent with the delimiter used in AppSearch.
+// See:
+// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/util/PrefixUtil.java;l=42;drc=ffaf979c6f0cbd26caafd7a9d07a6bad12fe3a2a
+
+constexpr char kAppSearchDatabaseDelimiter = '/';
+
 // A DynamicTrieKeyMapper stores its data across 3 arrays internally. Giving
 // each array 128KiB for storage means the entire DynamicTrieKeyMapper requires
 // 384KiB.
@@ -93,12 +103,12 @@
   std::unordered_set<SchemaTypeId> old_schema_type_ids_changed;
 
   std::unordered_map<std::string, int> old_types_and_index;
-  for (int i = 0; i < old_schema.types_size(); ++i) {
+  for (int i = 0; i < old_schema.types().size(); ++i) {
     old_types_and_index.emplace(old_schema.types(i).schema_type(), i);
   }
 
   std::unordered_map<std::string, int> new_types_and_index;
-  for (int i = 0; i < new_schema.types_size(); ++i) {
+  for (int i = 0; i < new_schema.types().size(); ++i) {
     new_types_and_index.emplace(new_schema.types(i).schema_type(), i);
   }
 
@@ -120,6 +130,46 @@
   return old_schema_type_ids_changed;
 }
 
+// Returns the database from the schema type name if it exists.
+//
+// The schema type is expected to be in the format of
+// <database><delimiter><actual_type_name>.
+//
+// Returns an empty string if the schema type name is not in the database
+// format.
+std::string GetDatabaseFromSchemaType(const std::string& schema_type,
+                                      char database_delimeter) {
+  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);
+  }
+  return database;
+}
+
+// For each schema type in the schema proto, parses out the database from the
+// type name, and sets it as the database field in the input proto in
+// place. The schema_type name field itself is not modified.
+//
+// If the schema type name does not contain an AppSearch database, then
+// SchemaTypeConfigProto is not modified.
+//
+// Returns:
+//   - True if any SchemaTypeConfigProto in the schema proto is rewritten.
+//   - False otherwise.
+bool ParseAndPopulateAppSearchDatabaseField(SchemaProto& schema_proto) {
+  bool populated_database_field = false;
+  for (auto& type : *schema_proto.mutable_types()) {
+    std::string database = GetDatabaseFromSchemaType(
+        type.schema_type(), kAppSearchDatabaseDelimiter);
+    if (type.database() != database) {
+      type.set_database(std::move(database));
+      populated_database_field = true;
+    }
+  }
+  return populated_database_field;
+}
+
 }  // namespace
 
 /* static */ libtextclassifier3::StatusOr<SchemaStore::Header>
@@ -202,36 +252,94 @@
 
 libtextclassifier3::StatusOr<std::unique_ptr<SchemaStore>> SchemaStore::Create(
     const Filesystem* filesystem, const std::string& base_dir,
-    const Clock* clock, InitializeStatsProto* initialize_stats) {
+    const Clock* clock, const FeatureFlags* feature_flags,
+    bool enable_schema_database, InitializeStatsProto* initialize_stats) {
   ICING_RETURN_ERROR_IF_NULL(filesystem);
   ICING_RETURN_ERROR_IF_NULL(clock);
+  ICING_RETURN_ERROR_IF_NULL(feature_flags);
 
   if (!filesystem->DirectoryExists(base_dir.c_str())) {
     return absl_ports::FailedPreconditionError(
         "Schema store base directory does not exist!");
   }
-  std::unique_ptr<SchemaStore> schema_store = std::unique_ptr<SchemaStore>(
-      new SchemaStore(filesystem, base_dir, clock));
+  std::unique_ptr<SchemaStore> schema_store =
+      std::unique_ptr<SchemaStore>(new SchemaStore(
+          filesystem, base_dir, clock, feature_flags, enable_schema_database));
   ICING_RETURN_IF_ERROR(schema_store->Initialize(initialize_stats));
   return schema_store;
 }
 
 libtextclassifier3::StatusOr<std::unique_ptr<SchemaStore>> SchemaStore::Create(
     const Filesystem* filesystem, const std::string& base_dir,
-    const Clock* clock, SchemaProto schema) {
+    const Clock* clock, const FeatureFlags* feature_flags, SchemaProto schema,
+    bool enable_schema_database) {
   ICING_RETURN_ERROR_IF_NULL(filesystem);
   ICING_RETURN_ERROR_IF_NULL(clock);
+  ICING_RETURN_ERROR_IF_NULL(feature_flags);
 
   if (!filesystem->DirectoryExists(base_dir.c_str())) {
     return absl_ports::FailedPreconditionError(
         "Schema store base directory does not exist!");
   }
-  std::unique_ptr<SchemaStore> schema_store = std::unique_ptr<SchemaStore>(
-      new SchemaStore(filesystem, base_dir, clock));
+  std::unique_ptr<SchemaStore> schema_store =
+      std::unique_ptr<SchemaStore>(new SchemaStore(
+          filesystem, base_dir, clock, feature_flags, enable_schema_database));
   ICING_RETURN_IF_ERROR(schema_store->Initialize(std::move(schema)));
   return schema_store;
 }
 
+/* static */ libtextclassifier3::Status
+SchemaStore::PopulateSchemaDatabaseFieldForSchemaFile(
+    const Filesystem* filesystem, const std::string& schema_filename) {
+  FileBackedProto<SchemaProto> schema_file(*filesystem, schema_filename);
+  auto schema_proto_or = schema_file.Read();
+  if (absl_ports::IsNotFound(schema_proto_or.status())) {
+    // Don't have an existing schema proto, that's fine
+    return libtextclassifier3::Status::OK;
+  } else if (!schema_proto_or.ok()) {
+    // Real error when trying to read the existing schema
+    return schema_proto_or.status();
+  }
+
+  SchemaProto schema_proto_copy = *schema_proto_or.ValueOrDie();
+  bool schema_changed =
+      ParseAndPopulateAppSearchDatabaseField(schema_proto_copy);
+  if (!schema_changed) {
+    // Nothing to do if the schema is not changed.
+    return libtextclassifier3::Status::OK;
+  }
+
+  // Create a temporary schema file and schema proto copy to update the
+  // schema.
+  std::string temp_schema_filename = schema_filename + ".tmp";
+  if (!filesystem->DeleteFile(temp_schema_filename.c_str())) {
+    return absl_ports::InternalError(
+        "Unable to delete temp schema file to prepare for schema database "
+        "migration.");
+  }
+
+  {
+    FileBackedProto<SchemaProto> temp_schema_file(*filesystem,
+                                                  temp_schema_filename);
+    ICING_RETURN_IF_ERROR(temp_schema_file.Write(
+        std::make_unique<SchemaProto>(schema_proto_copy)));
+  }
+
+  // Swap the temp schema file with the original schema file.
+  if (!filesystem->SwapFiles(temp_schema_filename.c_str(),
+                             schema_filename.c_str())) {
+    return absl_ports::InternalError(
+        "Unable to apply migrated schema with database due to failed swap!");
+  }
+  // Clean up the temp schema file.
+  if (!filesystem->DeleteFile(temp_schema_filename.c_str())) {
+    return absl_ports::InternalError(
+        "Unable to delete temp schema file after schema database migration.");
+  }
+
+  return libtextclassifier3::Status::OK;
+}
+
 /* static */ libtextclassifier3::Status SchemaStore::DiscardOverlaySchema(
     const Filesystem* filesystem, const std::string& base_dir, Header& header) {
   std::string header_filename = MakeHeaderFilename(base_dir);
@@ -252,7 +360,8 @@
 
 /* static */ libtextclassifier3::Status SchemaStore::MigrateSchema(
     const Filesystem* filesystem, const std::string& base_dir,
-    version_util::StateChange version_state_change, int32_t new_version) {
+    version_util::StateChange version_state_change, int32_t new_version,
+    bool perform_schema_database_migration) {
   if (!filesystem->DirectoryExists(base_dir.c_str())) {
     // Situations when schema store directory doesn't exist:
     // - Initializing new Icing instance: don't have to do anything now. The
@@ -264,6 +373,33 @@
     return libtextclassifier3::Status::OK;
   }
 
+  ICING_RETURN_IF_ERROR(HandleOverlaySchemaForVersionChange(
+      filesystem, base_dir, version_state_change, new_version));
+
+  // Perform schema database migration if needed.
+  // - This populates the the database field in the schema proto and writes it
+  //   to the schema file.
+  // - If the overlay schema file exists at this point, does the same for the
+  //   overlay schema.
+  if (perform_schema_database_migration) {
+    std::string base_schema_filename = MakeSchemaFilename(base_dir);
+    ICING_RETURN_IF_ERROR(PopulateSchemaDatabaseFieldForSchemaFile(
+        filesystem, base_schema_filename));
+
+    std::string overlay_schema_filename = MakeOverlaySchemaFilename(base_dir);
+    if (filesystem->FileExists(overlay_schema_filename.c_str())) {
+      ICING_RETURN_IF_ERROR(PopulateSchemaDatabaseFieldForSchemaFile(
+          filesystem, overlay_schema_filename));
+    }
+  }
+
+  return libtextclassifier3::Status::OK;
+}
+
+/* static */ libtextclassifier3::Status
+SchemaStore::HandleOverlaySchemaForVersionChange(
+    const Filesystem* filesystem, const std::string& base_dir,
+    version_util::StateChange version_state_change, int32_t new_version) {
   std::string overlay_schema_filename = MakeOverlaySchemaFilename(base_dir);
   if (!filesystem->FileExists(overlay_schema_filename.c_str())) {
     // The overlay doesn't exist. So there should be nothing particularly
@@ -329,12 +465,15 @@
 }
 
 SchemaStore::SchemaStore(const Filesystem* filesystem, std::string base_dir,
-                         const Clock* clock)
+                         const Clock* clock, const FeatureFlags* feature_flags,
+                         bool enable_schema_database)
     : filesystem_(filesystem),
       base_dir_(std::move(base_dir)),
       clock_(clock),
+      feature_flags_(feature_flags),
       schema_file_(std::make_unique<FileBackedProto<SchemaProto>>(
-          *filesystem, MakeSchemaFilename(base_dir_))) {}
+          *filesystem, MakeSchemaFilename(base_dir_))),
+      enable_schema_database_(enable_schema_database) {}
 
 SchemaStore::~SchemaStore() {
   if (has_schema_successfully_set_ && schema_file_ != nullptr &&
@@ -509,7 +648,7 @@
       ICING_RETURN_IF_ERROR(schema_file_->Write(std::move(base_schema_ptr)));
 
       // LINT.IfChange(min_overlay_version_compatibility)
-      // Although the current version is 4, the schema is compatible with
+      // Although the current version is 5, the schema is compatible with
       // version 1, so min_overlay_version_compatibility should be 1.
       int32_t min_overlay_version_compatibility = version_util::kVersionOne;
       // LINT.ThenChange(//depot/google3/icing/file/version-util.h:kVersion)
@@ -533,18 +672,23 @@
       SchemaUtil::BuildTransitiveInheritanceGraph(*schema_proto));
 
   reverse_schema_type_mapper_.clear();
+  database_type_map_.clear();
   type_config_map_.clear();
   schema_subtype_id_map_.clear();
   for (const SchemaTypeConfigProto& type_config : schema_proto->types()) {
-    std::string_view type_name = type_config.schema_type();
+    const std::string& database = type_config.database();
+    const std::string& type_name = type_config.schema_type();
     ICING_ASSIGN_OR_RETURN(SchemaTypeId type_id,
                            schema_type_mapper_->Get(type_name));
 
     // Build reverse_schema_type_mapper_
-    reverse_schema_type_mapper_.insert({type_id, std::string(type_name)});
+    reverse_schema_type_mapper_.insert({type_id, type_name});
+
+    // Build database_type_map_
+    database_type_map_[database].push_back(type_name);
 
     // Build type_config_map_
-    type_config_map_.insert({std::string(type_name), type_config});
+    type_config_map_.insert({type_name, type_config});
 
     // Build schema_subtype_id_map_
     std::unordered_set<SchemaTypeId>& subtype_id_set =
@@ -568,6 +712,9 @@
   ICING_ASSIGN_OR_RETURN(
       schema_type_manager_,
       SchemaTypeManager::Create(type_config_map_, schema_type_mapper_.get()));
+
+  scorable_property_manager_ = std::make_unique<ScorablePropertyManager>();
+
   return libtextclassifier3::Status::OK;
 }
 
@@ -661,11 +808,32 @@
   return schema_file_->Read();
 }
 
+libtextclassifier3::StatusOr<SchemaProto> SchemaStore::GetSchema(
+    const std::string& database) const {
+  if (!has_schema_successfully_set_) {
+    return absl_ports::NotFoundError("No schema found.");
+  }
+
+  const auto database_type_map_itr_ = database_type_map_.find(database);
+  if (database_type_map_itr_ == database_type_map_.end()) {
+    return absl_ports::NotFoundError(
+        absl_ports::StrCat("No schema found for database '", database, "'."));
+  }
+
+  SchemaProto schema_proto;
+  for (const std::string& type_name : database_type_map_itr_->second) {
+    ICING_ASSIGN_OR_RETURN(const SchemaTypeConfigProto* type_config,
+                           GetSchemaTypeConfig(type_name));
+    *schema_proto.add_types() = *type_config;
+  }
+  return schema_proto;
+}
+
 // TODO(cassiewang): Consider removing this definition of SetSchema if it's not
 // needed by production code. It's currently being used by our tests, but maybe
 // it's trivial to change our test code to also use the
 // SetSchema(SchemaProto&& new_schema)
-libtextclassifier3::StatusOr<const SchemaStore::SetSchemaResult>
+libtextclassifier3::StatusOr<SchemaStore::SetSchemaResult>
 SchemaStore::SetSchema(const SchemaProto& new_schema,
                        bool ignore_errors_and_delete_documents,
                        bool allow_circular_schema_definitions) {
@@ -673,92 +841,194 @@
                    allow_circular_schema_definitions);
 }
 
-libtextclassifier3::StatusOr<const SchemaStore::SetSchemaResult>
+libtextclassifier3::StatusOr<SchemaStore::SetSchemaResult>
 SchemaStore::SetSchema(SchemaProto&& new_schema,
                        bool ignore_errors_and_delete_documents,
                        bool allow_circular_schema_definitions) {
+  if (enable_schema_database_) {
+    // 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:
+    // - The new schema only contains types from a single database.
+    // - The new schema's type names are not already in use from other
+    // databases.
+    ICING_ASSIGN_OR_RETURN(std::string database,
+                           ValidateAndGetDatabase(new_schema));
+
+    // Step 2: Schema validation and delta computation -- try to get the
+    // existing schema for the database to compare to the new schema.
+    libtextclassifier3::StatusOr<SchemaProto> schema_proto =
+        GetSchema(database);
+    if (absl_ports::IsNotFound(schema_proto.status())) {
+      // Case 1: No preexisting schema for this database.
+      return SetInitialSchemaForDatabase(std::move(new_schema),
+                                         ignore_errors_and_delete_documents,
+                                         allow_circular_schema_definitions);
+    }
+
+    if (!schema_proto.ok()) {
+      // Case 2: Real error
+      return schema_proto.status();
+    }
+
+    // Case 3: At this point, we're guaranteed that we have an existing schema
+    // for this database.
+    const SchemaProto& old_schema = schema_proto.ValueOrDie();
+    return SetSchemaWithDatabaseOverride(std::move(new_schema), old_schema,
+                                         ignore_errors_and_delete_documents,
+                                         allow_circular_schema_definitions);
+  }
+
+  // Get the full schema if schema database is disabled.
+  libtextclassifier3::StatusOr<const SchemaProto*> schema_proto = GetSchema();
+  if (absl_ports::IsNotFound(schema_proto.status())) {
+    // Case 1: No preexisting schema
+    return SetInitialSchemaForDatabase(std::move(new_schema),
+                                       ignore_errors_and_delete_documents,
+                                       allow_circular_schema_definitions);
+  }
+
+  if (!schema_proto.ok()) {
+    // Case 2: Real error
+    return schema_proto.status();
+  }
+
+  // Case 3: At this point, we're guaranteed that we have an existing schema
+  const SchemaProto& old_schema = *schema_proto.ValueOrDie();
+  return SetSchemaWithDatabaseOverride(std::move(new_schema), old_schema,
+                                       ignore_errors_and_delete_documents,
+                                       allow_circular_schema_definitions);
+}
+
+libtextclassifier3::StatusOr<SchemaStore::SetSchemaResult>
+SchemaStore::SetInitialSchemaForDatabase(
+    SchemaProto new_schema, bool ignore_errors_and_delete_documents,
+    bool allow_circular_schema_definitions) {
   SetSchemaResult result;
 
-  auto schema_proto_or = GetSchema();
-  if (absl_ports::IsNotFound(schema_proto_or.status())) {
-    // We don't have a pre-existing schema, so anything is valid as long as the
-    // new schema is valid.
-    ICING_RETURN_IF_ERROR(
-        SchemaUtil::Validate(new_schema, allow_circular_schema_definitions));
+  ICING_RETURN_IF_ERROR(SchemaUtil::Validate(
+      new_schema, *feature_flags_, allow_circular_schema_definitions));
 
-    result.success = true;
-    for (const SchemaTypeConfigProto& type_config : new_schema.types()) {
-      result.schema_types_new_by_name.insert(type_config.schema_type());
-    }
-  } else if (!schema_proto_or.ok()) {
-    // Real error
-    return schema_proto_or.status();
-  } else {
-    // At this point, we're guaranteed that we have a schema.
-    const SchemaProto old_schema = *schema_proto_or.ValueOrDie();
-
-    // Assume we can set the schema unless proven otherwise.
-    result.success = true;
-
-    if (new_schema.SerializeAsString() == old_schema.SerializeAsString()) {
-      // Same schema as before. No need to update anything
-      return result;
-    }
-
-    // Different schema -- validate the new schema, track the differences and
-    // see if we can still write it
-    ICING_ASSIGN_OR_RETURN(
-        SchemaUtil::DependentMap new_dependent_map,
-        SchemaUtil::Validate(new_schema, allow_circular_schema_definitions));
-    SchemaUtil::SchemaDelta schema_delta =
-        SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                              new_dependent_map);
-
-    result.schema_types_new_by_name = std::move(schema_delta.schema_types_new);
-    result.schema_types_changed_fully_compatible_by_name =
-        std::move(schema_delta.schema_types_changed_fully_compatible);
-    result.schema_types_index_incompatible_by_name =
-        std::move(schema_delta.schema_types_index_incompatible);
-    result.schema_types_join_incompatible_by_name =
-        std::move(schema_delta.schema_types_join_incompatible);
-
-    for (const auto& schema_type : schema_delta.schema_types_deleted) {
-      // We currently don't support deletions, so mark this as not possible.
-      // This will change once we allow force-set schemas.
-      result.success = false;
-
-      result.schema_types_deleted_by_name.emplace(schema_type);
-
-      ICING_ASSIGN_OR_RETURN(SchemaTypeId schema_type_id,
-                             GetSchemaTypeId(schema_type));
-      result.schema_types_deleted_by_id.emplace(schema_type_id);
-    }
-
-    for (const auto& schema_type : schema_delta.schema_types_incompatible) {
-      // We currently don't support incompatible schemas, so mark this as
-      // not possible. This will change once we allow force-set schemas.
-      result.success = false;
-
-      result.schema_types_incompatible_by_name.emplace(schema_type);
-
-      ICING_ASSIGN_OR_RETURN(SchemaTypeId schema_type_id,
-                             GetSchemaTypeId(schema_type));
-      result.schema_types_incompatible_by_id.emplace(schema_type_id);
-    }
-
-    // SchemaTypeIds changing is fine, we can update the DocumentStore
-    result.old_schema_type_ids_changed =
-        SchemaTypeIdsChanged(old_schema, new_schema);
+  result.success = true;
+  for (const SchemaTypeConfigProto& type_config : new_schema.types()) {
+    result.schema_types_new_by_name.insert(type_config.schema_type());
   }
+  // 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)));
+  ICING_RETURN_IF_ERROR(ApplySchemaChange(std::move(full_new_schema)));
+  has_schema_successfully_set_ = true;
+
+  return result;
+}
+
+libtextclassifier3::StatusOr<SchemaStore::SetSchemaResult>
+SchemaStore::SetSchemaWithDatabaseOverride(
+    SchemaProto new_schema, const SchemaProto& old_schema,
+    bool ignore_errors_and_delete_documents,
+    bool allow_circular_schema_definitions) {
+  // Assume we can set the schema unless proven otherwise.
+  SetSchemaResult result;
+  result.success = true;
+
+  if (new_schema.SerializeAsString() == old_schema.SerializeAsString()) {
+    // Same schema as before. No need to update anything
+    return result;
+  }
+
+  // Different schema -- we need to validate the schema and track the
+  // differences to see if we can still write it.
+  //
+  // Validate the new schema and compute the delta between the old and new
+  // schema.
+  ICING_ASSIGN_OR_RETURN(
+      SchemaUtil::DependentMap new_dependent_map,
+      SchemaUtil::Validate(new_schema, *feature_flags_,
+                           allow_circular_schema_definitions));
+  SchemaUtil::SchemaDelta schema_delta = SchemaUtil::ComputeCompatibilityDelta(
+      old_schema, new_schema, new_dependent_map, *feature_flags_);
+
+  result.schema_types_new_by_name = std::move(schema_delta.schema_types_new);
+  result.schema_types_changed_fully_compatible_by_name =
+      std::move(schema_delta.schema_types_changed_fully_compatible);
+  result.schema_types_index_incompatible_by_name =
+      std::move(schema_delta.schema_types_index_incompatible);
+  result.schema_types_join_incompatible_by_name =
+      std::move(schema_delta.schema_types_join_incompatible);
+  result.schema_types_scorable_property_inconsistent_by_name =
+      std::move(schema_delta.schema_types_scorable_property_inconsistent);
+
+  for (const std::string& schema_type : schema_delta.schema_types_deleted) {
+    // We currently don't support deletions, so mark this as not possible.
+    // This will change once we allow force-set schemas.
+    result.success = false;
+
+    result.schema_types_deleted_by_name.emplace(schema_type);
+
+    ICING_ASSIGN_OR_RETURN(SchemaTypeId schema_type_id,
+                           GetSchemaTypeId(schema_type));
+    result.schema_types_deleted_by_id.emplace(schema_type_id);
+  }
+
+  for (const std::string& schema_type :
+       schema_delta.schema_types_incompatible) {
+    // We currently don't support incompatible schemas, so mark this as
+    // not possible. This will change once we allow force-set schemas.
+    result.success = false;
+
+    result.schema_types_incompatible_by_name.emplace(schema_type);
+
+    ICING_ASSIGN_OR_RETURN(SchemaTypeId schema_type_id,
+                           GetSchemaTypeId(schema_type));
+    result.schema_types_incompatible_by_id.emplace(schema_type_id);
+  }
+
+  // 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)));
+
+  // 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
+  // schema, and not on a per-database level.
+  //
+  // SchemaTypeIds changing is fine, we can update the DocumentStore.
+  ICING_ASSIGN_OR_RETURN(const SchemaProto* full_old_schema, GetSchema());
+  result.old_schema_type_ids_changed =
+      SchemaTypeIdsChanged(*full_old_schema, full_new_schema);
 
   // We can force set the schema if the caller has told us to ignore any errors
   result.success = result.success || ignore_errors_and_delete_documents;
 
+  // Step 3: Apply the schema change if success. This updates persisted files
+  // and derived data structures.
   if (result.success) {
-    ICING_RETURN_IF_ERROR(ApplySchemaChange(std::move(new_schema)));
+    ICING_RETURN_IF_ERROR(ApplySchemaChange(std::move(full_new_schema)));
     has_schema_successfully_set_ = true;
   }
 
+  // Convert schema types to SchemaTypeIds after the new schema is applied.
+  if (feature_flags_->enable_scorable_properties()) {
+    for (const std::string& schema_type :
+         result.schema_types_scorable_property_inconsistent_by_name) {
+      libtextclassifier3::StatusOr<SchemaTypeId> schema_type_id_or =
+          GetSchemaTypeId(schema_type);
+      if (!schema_type_id_or.ok()) {
+        if (absl_ports::IsNotFound(schema_type_id_or.status())) {
+          continue;
+        }
+        return schema_type_id_or.status();
+      }
+      result.schema_types_scorable_property_inconsistent_by_id.insert(
+          schema_type_id_or.ValueOrDie());
+    }
+  }
+
   return result;
 }
 
@@ -790,7 +1060,8 @@
   ICING_ASSIGN_OR_RETURN(
       std::unique_ptr<SchemaStore> new_schema_store,
       SchemaStore::Create(filesystem_, temp_schema_store_dir.dir(), clock_,
-                          std::move(new_schema)));
+                          feature_flags_, std::move(new_schema),
+                          enable_schema_database_));
 
   // Then we swap the new schema file + new derived files with the old files.
   if (!filesystem_->SwapFiles(base_dir_.c_str(),
@@ -880,6 +1151,15 @@
       .GetJoinablePropertyMetadata(schema_type_id, property_path);
 }
 
+libtextclassifier3::StatusOr<const JoinablePropertyMetadata*>
+SchemaStore::GetJoinablePropertyMetadata(
+    SchemaTypeId schema_type_id,
+    JoinablePropertyId joinable_property_id) const {
+  ICING_RETURN_IF_ERROR(CheckSchemaSet());
+  return schema_type_manager_->joinable_property_manager()
+      .GetJoinablePropertyMetadata(schema_type_id, joinable_property_id);
+}
+
 libtextclassifier3::StatusOr<JoinablePropertyGroup>
 SchemaStore::ExtractJoinableProperties(const DocumentProto& document) const {
   ICING_RETURN_IF_ERROR(CheckSchemaSet());
@@ -887,6 +1167,29 @@
       .ExtractJoinableProperties(document);
 }
 
+libtextclassifier3::StatusOr<std::optional<int>>
+SchemaStore::GetScorablePropertyIndex(SchemaTypeId schema_type_id,
+                                      std::string_view property_path) const {
+  ICING_RETURN_IF_ERROR(CheckSchemaSet());
+  if (!feature_flags_->enable_scorable_properties()) {
+    return std::nullopt;
+  }
+  return scorable_property_manager_->GetScorablePropertyIndex(
+      schema_type_id, property_path, type_config_map_,
+      reverse_schema_type_mapper_);
+}
+
+libtextclassifier3::StatusOr<
+    const std::vector<ScorablePropertyManager::ScorablePropertyInfo>*>
+SchemaStore::GetOrderedScorablePropertyInfo(SchemaTypeId schema_type_id) const {
+  ICING_RETURN_IF_ERROR(CheckSchemaSet());
+  if (!feature_flags_->enable_scorable_properties()) {
+    return nullptr;
+  }
+  return scorable_property_manager_->GetOrderedScorablePropertyInfo(
+      schema_type_id, type_config_map_, reverse_schema_type_mapper_);
+}
+
 libtextclassifier3::Status SchemaStore::PersistToDisk() {
   if (!has_schema_successfully_set_) {
     return libtextclassifier3::Status::OK;
@@ -903,7 +1206,7 @@
   storage_info.set_schema_store_size(
       Filesystem::SanitizeFileSize(directory_size));
   ICING_ASSIGN_OR_RETURN(const SchemaProto* schema, GetSchema(), storage_info);
-  storage_info.set_num_schema_types(schema->types_size());
+  storage_info.set_num_schema_types(schema->types().size());
   int total_sections = 0;
   int num_types_sections_exhausted = 0;
   for (const SchemaTypeConfigProto& type : schema->types()) {
@@ -1064,5 +1367,152 @@
   return blob_property_map;
 }
 
+libtextclassifier3::StatusOr<std::string> SchemaStore::ValidateAndGetDatabase(
+    const SchemaProto& new_schema) const {
+  std::string database;
+
+  if (!enable_schema_database_ || new_schema.types().empty()) {
+    return database;
+  }
+
+  database = new_schema.types(0).database();
+  // Loop through new_schema's types and validate it. The input SchemaProto
+  // contains a list of SchemaTypeConfigProtos without deduplication. We need to
+  // check that:
+  // 1. All SchemaTypeConfigProtos have the same database value.
+  // 2. The SchemaTypeConfigProtos's schema_type field is unique within both
+  //    new_schema, as well as the existing schema (recorded in
+  //    type_config_map_).
+  for (const SchemaTypeConfigProto& type_config : new_schema.types()) {
+    // Check database consistency.
+    if (database != type_config.database()) {
+      return absl_ports::InvalidArgumentError(
+          "SetSchema only accepts a SchemaProto with types from a single "
+          "database at a time. Please make separate calls for each database if "
+          "you need to set the schema for multiple databases.");
+    }
+
+    // Check type name uniqueness. This is only necessary if there is a
+    // pre-existing schema.
+    if (has_schema_successfully_set_) {
+      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 database;
+}
+
+libtextclassifier3::StatusOr<SchemaProto>
+SchemaStore::GetFullSchemaProtoWithUpdatedDb(
+    SchemaProto input_database_schema) const {
+  if (!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.
+    return input_database_schema;
+  }
+
+  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.
+  if (input_database_schema.types().empty()) {
+    return *schema_proto.ValueOrDie();
+  }
+
+  std::string input_database = input_database_schema.types(0).database();
+  if (database_type_map_.size() == 1 &&
+      database_type_map_.find(input_database) != database_type_map_.end()) {
+    // No other databases in the schema -- we can return the input database
+    // schema.
+    return input_database_schema;
+  }
+
+  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 the input_database, we replace the existing types with the input
+  //   types. An exisiting type is deleted if it's not included in
+  //   input_database.
+  // - If there are more input types than existing types for the input_database,
+  //   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 the
+  //   input_database, we shift all existing that come after input_database
+  //   forward.
+  // - For existing types from other databases, we add the types in their
+  //   original order to full_schema. Note that the type-ids of existing types
+  //   might still change if some types deleted in input_database as this will
+  //   cause all subsequent types ids to shift forward.
+  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()) {
+    const SchemaTypeConfigProto& existing_type_config =
+        existing_schema->types(existing_schema_index);
+    SchemaTypeConfigProto& input_type_config =
+        *input_database_schema.mutable_types(input_schema_index);
+
+    if (input_type_config.database() != input_database) {
+      return absl_ports::InvalidArgumentError(
+          "Can only update a single database at a time.");
+    }
+
+    if (existing_type_config.database() == input_database) {
+      // If the database is the one being updated by the input schema, replace
+      // the existing type with a type from the input schema.
+      *full_schema.add_types() = std::move(input_type_config);
+      ++input_schema_index;
+    } else {
+      *full_schema.add_types() = existing_type_config;
+    }
+    ++existing_schema_index;
+  }
+
+  // 2. Append remaining types to the end of the SchemaProto.
+  for (; input_schema_index < input_database_schema.types().size();
+       ++input_schema_index) {
+    // Case 1: Append all remaining types from the input schema. This happens
+    // when more types are added in input_database_schema than what's in the
+    // existing schema. In this case, we've used up the space for the database
+    // in the existing schema, so we can just append the rest of the types to
+    // the end.
+    SchemaTypeConfigProto& input_type_config =
+        *input_database_schema.mutable_types(input_schema_index);
+    *full_schema.add_types() = std::move(input_type_config);
+  }
+  for (; existing_schema_index < existing_schema->types().size();
+       ++existing_schema_index) {
+    // Case 2: Add remaining types from the existing schema, but skip the ones
+    // that are from input_database, since existing types from input_database
+    // are replaced with input_database_schema.
+    if (existing_schema->types(existing_schema_index).database() !=
+        input_database) {
+      *full_schema.add_types() = existing_schema->types(existing_schema_index);
+    }
+  }
+
+  return full_schema;
+}
+
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/schema/schema-store.h b/icing/schema/schema-store.h
index 77cd87f..e7b1e36 100644
--- a/icing/schema/schema-store.h
+++ b/icing/schema/schema-store.h
@@ -19,15 +19,18 @@
 #include <cstring>
 #include <limits>
 #include <memory>
+#include <optional>
 #include <string>
 #include <string_view>
 #include <unordered_map>
 #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/feature-flags.h"
 #include "icing/file/file-backed-proto.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/version-util.h"
@@ -40,11 +43,13 @@
 #include "icing/schema/joinable-property.h"
 #include "icing/schema/schema-type-manager.h"
 #include "icing/schema/schema-util.h"
+#include "icing/schema/scorable_property_manager.h"
 #include "icing/schema/section.h"
 #include "icing/store/document-filter-data.h"
 #include "icing/store/key-mapper.h"
 #include "icing/util/clock.h"
 #include "icing/util/crc32.h"
+#include "icing/util/status-macros.h"
 
 namespace icing {
 namespace lib {
@@ -226,6 +231,18 @@
     // but invalidated the joinable cache. Represented by the `schema_type`
     // field in the SchemaTypeConfigProto.
     std::unordered_set<std::string> schema_types_join_incompatible_by_name;
+
+    // Schema types that were changed in a way that was backwards compatible,
+    // but inconsistent with the old schema so that the scorable property cache
+    // needs to be re-generated.
+    std::unordered_set<SchemaTypeId>
+        schema_types_scorable_property_inconsistent_by_id;
+
+    // Schema types that were changed in a way that was backwards compatible,
+    // but inconsistent with the old schema so that the scorable property cache
+    // needs to be re-generated.
+    std::unordered_set<std::string>
+        schema_types_scorable_property_inconsistent_by_name;
   };
 
   struct ExpandedTypePropertyMask {
@@ -249,16 +266,20 @@
   //   INTERNAL_ERROR on any IO errors
   static libtextclassifier3::StatusOr<std::unique_ptr<SchemaStore>> Create(
       const Filesystem* filesystem, const std::string& base_dir,
-      const Clock* clock, InitializeStatsProto* initialize_stats = nullptr);
+      const Clock* clock, const FeatureFlags* feature_flags,
+      bool enable_schema_database = false,
+      InitializeStatsProto* initialize_stats = nullptr);
 
   // Migrates schema files (backup v.s. new schema) according to version state
-  // change.
+  // change. Also performs schema database migration and populates the database
+  // fields in the persisted schema file if necessary.
   //
   // Returns:
   //   OK on success or nothing to migrate
   static libtextclassifier3::Status MigrateSchema(
       const Filesystem* filesystem, const std::string& base_dir,
-      version_util::StateChange version_state_change, int32_t new_version);
+      version_util::StateChange version_state_change, int32_t new_version,
+      bool perform_schema_database_migration);
 
   // Discards all derived data in the schema store.
   //
@@ -280,30 +301,49 @@
   // Retrieve the current schema if it exists.
   //
   // Returns:
-  //   SchemaProto* if exists
-  //   INTERNAL_ERROR on any IO errors
-  //   NOT_FOUND_ERROR if a schema hasn't been set before
+  //   - SchemaProto* if exists
+  //   - INTERNAL_ERROR on any IO errors
+  //   - NOT_FOUND_ERROR if a schema hasn't been set before
   libtextclassifier3::StatusOr<const SchemaProto*> GetSchema() const;
 
+  // Retrieve the current schema for a given database if it exists.
+  //
+  // This is an expensive operation. Use GetSchema() when retrieving the entire
+  // schema, or if there is only a single database in the schema store.
+  //
+  // Returns:
+  //   - SchemaProto* containing only schema types from the database, if exists
+  //   - INTERNAL_ERROR on any IO errors
+  //   - NOT_FOUND_ERROR if the database doesn't exist in the schema, or if a
+  //     schema hasn't been set before
+  libtextclassifier3::StatusOr<SchemaProto> GetSchema(
+      const std::string& database) const;
+
   // Update our current schema if it's compatible. Does not accept incompatible
-  // schema. Compatibility rules defined by
-  // SchemaUtil::ComputeCompatibilityDelta.
+  // schema or schema with types from multiple databases. Compatibility rules
+  // defined by SchemaUtil::ComputeCompatibilityDelta.
+  //
+  // The schema types in the new schema proto must all be from a single
+  // database. Does not support setting schema types across multiple databases
+  // at once.
   //
   // If ignore_errors_and_delete_documents is set to true, then incompatible
   // schema are allowed and we'll force set the schema, meaning
   // SetSchemaResult.success will always be true.
   //
   // Returns:
-  //   SetSchemaResult that encapsulates the differences between the old and new
-  //   schema, as well as if the new schema can be set.
-  //   INTERNAL_ERROR on any IO errors
-  libtextclassifier3::StatusOr<const SetSchemaResult> SetSchema(
-      const SchemaProto& new_schema,
-      bool ignore_errors_and_delete_documents,
+  //   - SetSchemaResult that encapsulates the differences between the old and
+  //     new schema, as well as if the new schema can be set.
+  //   - INTERNAL_ERROR on any IO errors
+  //   - ALREADY_EXISTS_ERROR if type names in the new schema are already in use
+  //     by a different database.
+  //   - INVALID_ARGUMENT_ERROR if the schema is invalid, or if the schema types
+  //     are from multiple databases (once schema database is enabled).
+  libtextclassifier3::StatusOr<SetSchemaResult> SetSchema(
+      const SchemaProto& new_schema, bool ignore_errors_and_delete_documents,
       bool allow_circular_schema_definitions);
-  libtextclassifier3::StatusOr<const SetSchemaResult> SetSchema(
-      SchemaProto&& new_schema,
-      bool ignore_errors_and_delete_documents,
+  libtextclassifier3::StatusOr<SetSchemaResult> SetSchema(
+      SchemaProto&& new_schema, bool ignore_errors_and_delete_documents,
       bool allow_circular_schema_definitions);
 
   // Get the SchemaTypeConfigProto of schema_type name.
@@ -401,6 +441,17 @@
   GetJoinablePropertyMetadata(SchemaTypeId schema_type_id,
                               const std::string& property_path) const;
 
+  // Returns the JoinablePropertyMetadata associated with joinable_property_id
+  // that's in the SchemaTypeId.
+  //
+  // Returns:
+  //   Valid pointer to JoinablePropertyMetadata on success
+  //   FAILED_PRECONDITION if schema hasn't been set yet
+  //   INVALID_ARGUMENT if schema type id or joinable property id is invalid
+  libtextclassifier3::StatusOr<const JoinablePropertyMetadata*>
+  GetJoinablePropertyMetadata(SchemaTypeId schema_type_id,
+                              JoinablePropertyId joinable_property_id) const;
+
   // Extracts all joinable property contents of different types from the given
   // document and group them by joinable value type.
   // - Joinable properties are sorted by joinable property id in ascending
@@ -416,6 +467,19 @@
   libtextclassifier3::StatusOr<JoinablePropertyGroup> ExtractJoinableProperties(
       const DocumentProto& document) const;
 
+  // Returns the quantization type for the given schema_type_id and section_id.
+  //
+  // Returns:
+  //   - The quantization type on success.
+  //   - INVALID_ARGUMENT_ERROR if schema_type_id or section_id is invalid.
+  //   - Any error from schema store.
+  libtextclassifier3::StatusOr<EmbeddingIndexingConfig::QuantizationType::Code>
+  GetQuantizationType(SchemaTypeId schema_type_id, SectionId section_id) const {
+    ICING_ASSIGN_OR_RETURN(const SectionMetadata* section_metadata,
+                           GetSectionMetadata(schema_type_id, section_id));
+    return section_metadata->quantization_type;
+  }
+
   // Syncs all the data changes to disk.
   //
   // Returns:
@@ -445,6 +509,32 @@
   libtextclassifier3::StatusOr<const std::vector<SectionMetadata>*>
   GetSectionMetadata(const std::string& schema_type) const;
 
+  // Gets the index of the given |property_path|, where the index N means that
+  // it is the Nth scorable property path in the schema config of the given
+  // |schema_type_id|, in lexicographical order.
+  //
+  // Returns:
+  //   - Index on success
+  //   - std::nullopt if the |property_path| doesn't point to a scorable
+  //     property under the |schema_type_id|
+  //   - FAILED_PRECONDITION if the schema hasn't been set yet
+  //   - INVALID_ARGUMENT if |schema_type_id| is invalid
+  libtextclassifier3::StatusOr<std::optional<int>> GetScorablePropertyIndex(
+      SchemaTypeId schema_type_id, std::string_view property_path) const;
+
+  // Returns the list of ScorablePropertyInfo for the given |schema_type_id|,
+  // in lexicographical order of its property path.
+  //
+  // Returns:
+  //   - Vector of scorable property info on success. The vector can be empty
+  //     if no scorable property is found under the schema config of
+  //     |schema_type_id|.
+  //   - FAILED_PRECONDITION if the schema hasn't been set yet
+  //   - INVALID_ARGUMENT if |schema_type_id| is invalid
+  libtextclassifier3::StatusOr<
+      const std::vector<ScorablePropertyManager::ScorablePropertyInfo>*>
+  GetOrderedScorablePropertyInfo(SchemaTypeId schema_type_id) const;
+
   // Calculates the StorageInfo for the Schema Store.
   //
   // If an IO error occurs while trying to calculate the value for a field, then
@@ -489,11 +579,13 @@
   //   INTERNAL_ERROR on any IO errors
   static libtextclassifier3::StatusOr<std::unique_ptr<SchemaStore>> Create(
       const Filesystem* filesystem, const std::string& base_dir,
-      const Clock* clock, SchemaProto schema);
+      const Clock* clock, const FeatureFlags* feature_flags, SchemaProto schema,
+      bool enable_schema_database);
 
   // Use SchemaStore::Create instead.
   explicit SchemaStore(const Filesystem* filesystem, std::string base_dir,
-                       const Clock* clock);
+                       const Clock* clock, const FeatureFlags* feature_flags,
+                       bool enable_schema_database);
 
   // Deletes the overlay schema and ensures that the Header is correctly set.
   //
@@ -504,6 +596,27 @@
       const Filesystem* filesystem, const std::string& base_dir,
       Header& header);
 
+  // Handles the overlay schema after a version change by deleting it if it is
+  // no longer compatible with the new version.
+  //
+  // Requires: base_dir exists.
+  //
+  // Returns:
+  //   OK on success
+  //   INTERNAL_ERROR on any IO errors
+  static libtextclassifier3::Status HandleOverlaySchemaForVersionChange(
+      const Filesystem* filesystem, const std::string& base_dir,
+      version_util::StateChange version_state_change, int32_t new_version);
+
+  // Populates the schema database field in the schema proto that is stored in
+  // the input schema file.
+  //
+  // Returns:
+  //   OK on success or nothing to migrate
+  //   INTERNAL_ERROR on IO error
+  static libtextclassifier3::Status PopulateSchemaDatabaseFieldForSchemaFile(
+      const Filesystem* filesystem, const std::string& schema_filename);
+
   // Verifies that there is no error retrieving a previously set schema. Then
   // initializes like normal.
   //
@@ -593,9 +706,101 @@
   //     Or an invalid schema configuration is present.
   libtextclassifier3::Status LoadSchema();
 
+  // Sets the schema for a database for the first time.
+  //
+  // Note that when schema database is disabled, this function sets the entire
+  // schema, with all under the default empty database.
+  //
+  // Requires:
+  //   - All types in new_schema are from the same database.
+  //   - new_schema does not contain type names that are already in use by a
+  //     different database.
+  //
+  // Returns:
+  //   - SetSchemaResult that indicates if the new schema can be set.
+  //   - INTERNAL_ERROR on any IO errors.
+  //   - INVALID_ARGUMENT_ERROR if the schema is invalid.
+  libtextclassifier3::StatusOr<SchemaStore::SetSchemaResult>
+  SetInitialSchemaForDatabase(SchemaProto new_schema,
+                              bool ignore_errors_and_delete_documents,
+                              bool allow_circular_schema_definitions);
+
+  // Sets the schema for a database, overriding any existing schema for that
+  // database.
+  //
+  // Note that when schema database is disabled, this function sets and
+  // overrides the entire schema.
+  //
+  // Requires:
+  //   - All types in new_schema are from the same database.
+  //   - new_schema does not contain type names that are already in use by a
+  //     different database.
+  //
+  // Returns:
+  //   - SetSchemaResult that encapsulates the differences between the old and
+  //     new schema, as well as if the new schema can be set.
+  //   - INTERNAL_ERROR on any IO errors.
+  //   - INVALID_ARGUMENT_ERROR if the schema is invalid.
+  libtextclassifier3::StatusOr<SchemaStore::SetSchemaResult>
+  SetSchemaWithDatabaseOverride(SchemaProto new_schema,
+                                const SchemaProto& old_schema,
+                                bool ignore_errors_and_delete_documents,
+                                bool allow_circular_schema_definitions);
+
+  // Initial validation on the SchemaProto for SetSchema. This is intended as a
+  // preliminary check before any expensive operations are performed during
+  // `SetSchema::Validate`. Returns the schema's database if it's valid.
+  //
+  // Note that when schema database is disabled, any schema input is valid and
+  // an empty string is returned as the database.
+  //
+  // Checks that:
+  // - The new schema only contains types from a single database.
+  // - The schema's type names are not already in use in other databases. This
+  //   is done outside of `SchemaUtil::Validate` because we need to know all
+  //   existing type names, which is stored in the SchemaStore and not known to
+  //   SchemaUtil.
+  //
+  // Returns:
+  //   - new_schema's database on success
+  //   - INVALID_ARGUMENT_ERROR if new_schema contains types from multiple
+  //     databases
+  //   - ALREADY_EXISTS_ERROR if new_schema's types names are not unique
+  libtextclassifier3::StatusOr<std::string> ValidateAndGetDatabase(
+      const SchemaProto& new_schema) const;
+
+  // Returns a SchemaProto representing the full schema, which is a combination
+  // of the existing schema and the input database schema.
+  //
+  // For the database being updated by the input database schema:
+  // - If the existing schema does not contain the database, the input types
+  //   are appended to the end of the SchemaProto, without changing the order
+  //   of the existing schema types.
+  // - Otherwise, the existing schema types are replaced with types from the
+  //   input database schema in their original position in the existing
+  //   SchemaProto.
+  //   - Types from input_database_schema are added in the order in which they
+  //     appear.
+  //   - If more types are added to the database, the additional types are
+  //     appended at the end of the SchemaProto, without changing the order of
+  //     existing types from unaffected databases.
+  //
+  // Requires:
+  //   - input_database_schema must not contain types from multiple databases.
+  //
+  // 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 contains types from multiple
+  //     databases.
+  libtextclassifier3::StatusOr<SchemaProto> GetFullSchemaProtoWithUpdatedDb(
+      SchemaProto input_database_schema) const;
+
   const Filesystem* filesystem_;
   std::string base_dir_;
   const Clock* clock_;
+  const FeatureFlags* feature_flags_;  // Does not own.
 
   // Used internally to indicate whether the class has been successfully
   // initialized with a valid schema. Will be false if Initialize failed or no
@@ -616,6 +821,17 @@
   // map of schema_type_mapper_.
   std::unordered_map<SchemaTypeId, std::string> reverse_schema_type_mapper_;
 
+  // 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
+  // the types (i.e. the order in which they appear in the input SchemaProto
+  // during SetSchema), so that we can return the correct SchemaProto for
+  // GetSchema.
+  //
+  // This keeps track of the type configs defined in each database, which allows
+  // schema operations to be performed on a per-database basis.
+  std::unordered_map<std::string, std::vector<std::string>> database_type_map_;
+
   // A hash map of (type config name -> type config), allows faster lookup of
   // type config in schema. The O(1) type config access makes schema-related and
   // section-related operations faster.
@@ -635,7 +851,18 @@
   // metadata for all Schemas.
   std::unique_ptr<const SchemaTypeManager> schema_type_manager_;
 
+  // Used to cache and manage the schema's scorable properties.
+  std::unique_ptr<ScorablePropertyManager> scorable_property_manager_;
+
   std::unique_ptr<Header> header_;
+
+  // Whether to use the database field for the schema.
+  //
+  // This is a temporary flag to control the rollout of the schema database. It
+  // affects the `SetSchema` and `GetSchema(std::string database)` methods.
+  // TODO - b/337913932: Remove this flag once the schema database is fully
+  // rolled out.
+  bool enable_schema_database_ = false;
 };
 
 }  // namespace lib
diff --git a/icing/schema/schema-store_test.cc b/icing/schema/schema-store_test.cc
index b2da7fa..271ab4a 100644
--- a/icing/schema/schema-store_test.cc
+++ b/icing/schema/schema-store_test.cc
@@ -16,6 +16,7 @@
 
 #include <cstdint>
 #include <memory>
+#include <optional>
 #include <string>
 #include <utility>
 #include <vector>
@@ -25,6 +26,7 @@
 #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-proto.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/mock-filesystem.h"
@@ -40,6 +42,7 @@
 #include "icing/store/document-filter-data.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
+#include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 #include "icing/util/crc32.h"
 
@@ -66,6 +69,7 @@
 class SchemaStoreTest : public ::testing::Test {
  protected:
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     test_dir_ = GetTestTempDir() + "/icing";
     schema_store_dir_ = test_dir_ + "/schema_store";
     filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str());
@@ -85,7 +89,8 @@
                                    PropertyConfigBuilder()
                                        .SetName("timestamp")
                                        .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
-                                       .SetCardinality(CARDINALITY_OPTIONAL)))
+                                       .SetCardinality(CARDINALITY_OPTIONAL)
+                                       .SetScorableType(SCORABLE_TYPE_ENABLED)))
                   .Build();
   }
 
@@ -101,6 +106,7 @@
     ASSERT_TRUE(filesystem_.DeleteDirectoryRecursively(test_dir_.c_str()));
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   std::string test_dir_;
   std::string schema_store_dir_;
@@ -108,9 +114,21 @@
   FakeClock fake_clock_;
 };
 
-TEST_F(SchemaStoreTest, CreationWithNullPointerShouldFail) {
+TEST_F(SchemaStoreTest, CreationWithFilesystemNullPointerShouldFail) {
   EXPECT_THAT(SchemaStore::Create(/*filesystem=*/nullptr, schema_store_dir_,
-                                  &fake_clock_),
+                                  &fake_clock_, feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+}
+
+TEST_F(SchemaStoreTest, CreationWithClockNullPointerShouldFail) {
+  EXPECT_THAT(SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                  /*clock=*/nullptr, feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+}
+
+TEST_F(SchemaStoreTest, CreationWithFeatureFlagsNullPointerShouldFail) {
+  EXPECT_THAT(SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                                  /*feature_flags=*/nullptr),
               StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 }
 
@@ -127,7 +145,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
@@ -143,7 +162,8 @@
               IsOkAndHolds(Eq(expected_checksum)));
   SectionMetadata expected_metadata(/*id_in=*/0, TYPE_STRING, TOKENIZER_PLAIN,
                                     TERM_MATCH_EXACT, NUMERIC_MATCH_UNKNOWN,
-                                    EMBEDDING_INDEXING_UNKNOWN, "prop1");
+                                    EMBEDDING_INDEXING_UNKNOWN,
+                                    QUANTIZATION_TYPE_NONE, "prop1");
   EXPECT_THAT(move_constructed_schema_store.GetSectionMetadata("type_a"),
               IsOkAndHolds(Pointee(ElementsAre(expected_metadata))));
 }
@@ -161,7 +181,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema1, /*ignore_errors_and_delete_documents=*/false,
@@ -181,7 +202,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> move_assigned_schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema2, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/false));
@@ -194,7 +216,8 @@
               IsOkAndHolds(Eq(expected_checksum)));
   SectionMetadata expected_metadata(/*id_in=*/0, TYPE_STRING, TOKENIZER_PLAIN,
                                     TERM_MATCH_EXACT, NUMERIC_MATCH_UNKNOWN,
-                                    EMBEDDING_INDEXING_UNKNOWN, "prop1");
+                                    EMBEDDING_INDEXING_UNKNOWN,
+                                    QUANTIZATION_TYPE_NONE, "prop1");
   EXPECT_THAT(move_assigned_schema_store->GetSectionMetadata("type_a"),
               IsOkAndHolds(Pointee(ElementsAre(expected_metadata))));
 }
@@ -203,7 +226,8 @@
   {
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
 
     // Set it for the first time
     SchemaStore::SetSchemaResult result;
@@ -234,16 +258,17 @@
                     serialized_schema.size());
 
   // If ground truth was corrupted, we won't know what to do
-  EXPECT_THAT(
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_),
-      StatusIs(libtextclassifier3::StatusCode::INTERNAL));
+  EXPECT_THAT(SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                                  feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::INTERNAL));
 }
 
 TEST_F(SchemaStoreTest, RecoverCorruptDerivedFileOk) {
   {
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
 
     // Set it for the first time
     SchemaStore::SetSchemaResult result;
@@ -258,6 +283,16 @@
     EXPECT_THAT(*actual_schema, EqualsProto(schema_));
 
     EXPECT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(0));
+
+    // Scorable property manager working as expected.
+    EXPECT_THAT(schema_store->GetOrderedScorablePropertyInfo(
+                    /*schema_type_id=*/0),
+                IsOkAndHolds(Pointee(ElementsAre(
+                    EqualsScorablePropertyInfo("timestamp", TYPE_INT64)))));
+    EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                    /*schema_type_id=*/0,
+                    /*property_path=*/"timestamp"),
+                IsOkAndHolds(0));
   }
 
   // "Corrupt" the derived SchemaTypeIds by deleting the entire directory. This
@@ -273,7 +308,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
       SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
-                          &initialize_stats));
+                          feature_flags_.get(),
+                          /*enable_schema_database=*/false, &initialize_stats));
   EXPECT_THAT(initialize_stats.schema_store_recovery_cause(),
               Eq(InitializeStatsProto::IO_ERROR));
   EXPECT_THAT(initialize_stats.schema_store_recovery_latency_ms(), Eq(123));
@@ -283,13 +319,24 @@
                              schema_store->GetSchema());
   EXPECT_THAT(*actual_schema, EqualsProto(schema_));
   EXPECT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(0));
+
+  // Scorable property manager working as expected.
+  EXPECT_THAT(schema_store->GetOrderedScorablePropertyInfo(
+                  /*schema_type_id=*/0),
+              IsOkAndHolds(Pointee(ElementsAre(
+                  EqualsScorablePropertyInfo("timestamp", TYPE_INT64)))));
+  EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                  /*schema_type_id=*/0,
+                  /*property_path=*/"timestamp"),
+              IsOkAndHolds(0));
 }
 
 TEST_F(SchemaStoreTest, RecoverDiscardDerivedFilesOk) {
   {
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
 
     // Set it for the first time
     SchemaStore::SetSchemaResult result;
@@ -304,6 +351,16 @@
     EXPECT_THAT(*actual_schema, EqualsProto(schema_));
 
     EXPECT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(0));
+
+    // Scorable property manager working as expected.
+    EXPECT_THAT(schema_store->GetOrderedScorablePropertyInfo(
+                    /*schema_type_id=*/0),
+                IsOkAndHolds(Pointee(ElementsAre(
+                    EqualsScorablePropertyInfo("timestamp", TYPE_INT64)))));
+    EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                    /*schema_type_id=*/0,
+                    /*property_path=*/"timestamp"),
+                IsOkAndHolds(0));
   }
 
   ICING_ASSERT_OK(
@@ -314,7 +371,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
       SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
-                          &initialize_stats));
+                          feature_flags_.get(),
+                          /*enable_schema_database=*/false, &initialize_stats));
   EXPECT_THAT(initialize_stats.schema_store_recovery_cause(),
               Eq(InitializeStatsProto::IO_ERROR));
   EXPECT_THAT(initialize_stats.schema_store_recovery_latency_ms(), Eq(123));
@@ -324,13 +382,24 @@
                              schema_store->GetSchema());
   EXPECT_THAT(*actual_schema, EqualsProto(schema_));
   EXPECT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(0));
+
+  // Scorable property manager working as expected.
+  EXPECT_THAT(schema_store->GetOrderedScorablePropertyInfo(
+                  /*schema_type_id=*/0),
+              IsOkAndHolds(Pointee(ElementsAre(
+                  EqualsScorablePropertyInfo("timestamp", TYPE_INT64)))));
+  EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                  /*schema_type_id=*/0,
+                  /*property_path=*/"timestamp"),
+              IsOkAndHolds(0));
 }
 
 TEST_F(SchemaStoreTest, RecoverBadChecksumOk) {
   {
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
 
     // Set it for the first time
     SchemaStore::SetSchemaResult result;
@@ -345,6 +414,16 @@
     EXPECT_THAT(*actual_schema, EqualsProto(schema_));
 
     EXPECT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(0));
+
+    // Scorable property manager working as expected.
+    EXPECT_THAT(schema_store->GetOrderedScorablePropertyInfo(
+                    /*schema_type_id=*/0),
+                IsOkAndHolds(Pointee(ElementsAre(
+                    EqualsScorablePropertyInfo("timestamp", TYPE_INT64)))));
+    EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                    /*schema_type_id=*/0,
+                    /*property_path=*/"timestamp"),
+                IsOkAndHolds(0));
   }
 
   // Change the SchemaStore's header combined checksum so that it won't match
@@ -360,19 +439,31 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // Everything looks fine, ground truth and derived data
   ICING_ASSERT_OK_AND_ASSIGN(const SchemaProto* actual_schema,
                              schema_store->GetSchema());
   EXPECT_THAT(*actual_schema, EqualsProto(schema_));
   EXPECT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(0));
+
+  // Scorable property manager working as expected.
+  EXPECT_THAT(schema_store->GetOrderedScorablePropertyInfo(
+                  /*schema_type_id=*/0),
+              IsOkAndHolds(Pointee(ElementsAre(
+                  EqualsScorablePropertyInfo("timestamp", TYPE_INT64)))));
+  EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                  /*schema_type_id=*/0,
+                  /*property_path=*/"timestamp"),
+              IsOkAndHolds(0));
 }
 
 TEST_F(SchemaStoreTest, CreateNoPreviousSchemaOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // The apis to retrieve information about the schema should fail gracefully.
   EXPECT_THAT(store->GetSchema(),
@@ -406,7 +497,8 @@
 TEST_F(SchemaStoreTest, CreateWithPreviousSchemaOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   SchemaStore::SetSchemaResult result;
   result.success = true;
@@ -417,9 +509,9 @@
               IsOkAndHolds(EqualsSetSchemaResult(result)));
 
   schema_store.reset();
-  EXPECT_THAT(
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_),
-      IsOk());
+  EXPECT_THAT(SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                                  feature_flags_.get()),
+              IsOk());
 }
 
 TEST_F(SchemaStoreTest, MultipleCreateOk) {
@@ -434,7 +526,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   SchemaStore::SetSchemaResult result;
   result.success = true;
@@ -454,13 +547,23 @@
   EXPECT_THAT(section_group.integer_sections[0].content,
               ElementsAre(kDefaultTimestamp));
 
+  // Scorable property manager working as expected.
+  EXPECT_THAT(schema_store->GetOrderedScorablePropertyInfo(
+                  /*schema_type_id=*/0),
+              IsOkAndHolds(Pointee(ElementsAre(
+                  EqualsScorablePropertyInfo("timestamp", TYPE_INT64)))));
+  EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                  /*schema_type_id=*/0,
+                  /*property_path=*/"timestamp"),
+              IsOkAndHolds(0));
+
   // Verify that our persisted data is ok
   EXPECT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(0));
 
   schema_store.reset();
   ICING_ASSERT_OK_AND_ASSIGN(
-      schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      schema_store, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                        &fake_clock_, feature_flags_.get()));
 
   // Verify that our in-memory structures are ok
   EXPECT_THAT(schema_store->GetSchemaTypeConfig("email"),
@@ -473,6 +576,16 @@
   EXPECT_THAT(section_group.integer_sections[0].content,
               ElementsAre(kDefaultTimestamp));
 
+  // Scorable property manager working as expected.
+  EXPECT_THAT(schema_store->GetOrderedScorablePropertyInfo(
+                  /*schema_type_id=*/0),
+              IsOkAndHolds(Pointee(ElementsAre(
+                  EqualsScorablePropertyInfo("timestamp", TYPE_INT64)))));
+  EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                  /*schema_type_id=*/0,
+                  /*property_path=*/"timestamp"),
+              IsOkAndHolds(0));
+
   // Verify that our persisted data is ok
   EXPECT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(0));
 }
@@ -480,7 +593,8 @@
 TEST_F(SchemaStoreTest, SetNewSchemaOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // Set it for the first time
   SchemaStore::SetSchemaResult result;
@@ -495,10 +609,81 @@
   EXPECT_THAT(*actual_schema, EqualsProto(schema_));
 }
 
+TEST_F(SchemaStoreTest, SetNewSchemaInDifferentDatabaseOk) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*enable_schema_database=*/true,
+                          /*initialize_stats=*/nullptr));
+
+  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");
+  EXPECT_THAT(schema_store->SetSchema(
+                  db1_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/false),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+  EXPECT_THAT(schema_store->GetSchema(),
+              IsOkAndHolds(Pointee(EqualsProto(db1_schema))));
+  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();
+  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(
+                  db2_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/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();
+  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)));
+}
+
 TEST_F(SchemaStoreTest, SetSameSchemaOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // Set it for the first time
   SchemaStore::SetSchemaResult result;
@@ -523,10 +708,577 @@
   EXPECT_THAT(*actual_schema, EqualsProto(schema_));
 }
 
+TEST_F(SchemaStoreTest, SetSameDatabaseSchemaOk) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*enable_schema_database=*/true,
+                          /*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();
+  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(
+                  db1_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/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");
+  EXPECT_THAT(schema_store->SetSchema(
+                  db2_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/false),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+  ICING_ASSERT_OK_AND_ASSIGN(const SchemaProto* actual_full_schema,
+                             schema_store->GetSchema());
+  EXPECT_THAT(*actual_full_schema, EqualsProto(expected_full_schema));
+
+  // Reset db1 with the same SchemaProto. The schema should be exactly the same.
+  result = SchemaStore::SetSchemaResult();
+  result.success = true;
+  EXPECT_THAT(
+      schema_store->SetSchema(db1_schema,
+                              /*ignore_errors_and_delete_documents=*/false,
+                              /*allow_circular_schema_definitions=*/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"),
+              IsOkAndHolds(EqualsProto(db1_schema)));
+  EXPECT_THAT(schema_store->GetSchema("db2"),
+              IsOkAndHolds(EqualsProto(db2_schema)));
+}
+
+TEST_F(SchemaStoreTest, SetDatabaseReorderedTypesPreservesSchemaTypeIds) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*enable_schema_database=*/true,
+                          /*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();
+
+  // 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(
+                  db1_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/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(
+                  db2_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/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(
+                  db3_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/false),
+              IsOkAndHolds(EqualsSetSchemaResult(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 with the types reordered. The expected full schema will be
+  // different, but the SchemaTypeIds for db1 and db3 should not change.
+  db2_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2_message")
+                       .SetDatabase("db2"))
+          .AddType(
+              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
+          .Build();
+  expected_full_schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder().SetType("db1_email").SetDatabase("db1"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db1_message")
+                       .SetDatabase("db1"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2_message")
+                       .SetDatabase("db2"))
+          .AddType(
+              SchemaTypeConfigBuilder().SetType("db2_email").SetDatabase("db2"))
+          .AddType(
+              SchemaTypeConfigBuilder().SetType("db3_email").SetDatabase("db3"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db3_message")
+                       .SetDatabase("db3"))
+          .Build();
+  result = SchemaStore::SetSchemaResult();
+  result.success = true;
+  // Only db2's schema type ids should change.
+  result.old_schema_type_ids_changed.insert(2);
+  result.old_schema_type_ids_changed.insert(3);
+  EXPECT_THAT(
+      schema_store->SetSchema(db2_schema,
+                              /*ignore_errors_and_delete_documents=*/false,
+                              /*allow_circular_schema_definitions=*/false),
+      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, SetDatabaseAddedTypesPreservesSchemaTypeIds) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*enable_schema_database=*/true,
+                          /*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();
+
+  // 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(
+                  db1_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/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(
+                  db2_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/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(
+                  db3_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/false),
+              IsOkAndHolds(EqualsSetSchemaResult(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 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();
+  result = SchemaStore::SetSchemaResult();
+  result.success = true;
+  result.schema_types_new_by_name.insert("db2_recipient");
+  EXPECT_THAT(
+      schema_store->SetSchema(db2_schema,
+                              /*ignore_errors_and_delete_documents=*/false,
+                              /*allow_circular_schema_definitions=*/false),
+      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, SetDatabaseDeletedTypesOk) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*enable_schema_database=*/true,
+                          /*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(
+                  db1_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/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(
+                  db2_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/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(
+                  db3_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/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(
+                  db2_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/false),
+              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_email")  // 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_recipient")  // 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));
+
+  // Reset db2 and delete some types. All types that were originally added after
+  // db2 should have their type ids changed.
+  db2_schema = SchemaBuilder()
+                   .AddType(SchemaTypeConfigBuilder()
+                                .SetType("db2_message")
+                                .SetDatabase("db2"))
+                   .Build();
+  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 2
+                       .SetDatabase("db2"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db3_email")  // SchemaTypeId 3
+                       .SetDatabase("db3"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db3_message")  // SchemaTypeId 4
+                       .SetDatabase("db3"))
+          .Build();
+  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
+  EXPECT_THAT(
+      schema_store->SetSchema(db2_schema,
+                              /*ignore_errors_and_delete_documents=*/true,
+                              /*allow_circular_schema_definitions=*/false),
+      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, SetEmptySchemaInDifferentDatabaseOk) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*enable_schema_database=*/true,
+                          /*initialize_stats=*/nullptr));
+
+  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");
+  EXPECT_THAT(schema_store->SetSchema(
+                  db1_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/false),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+  EXPECT_THAT(schema_store->GetSchema(),
+              IsOkAndHolds(Pointee(EqualsProto(db1_schema))));
+  EXPECT_THAT(schema_store->GetSchema("db1"),
+              IsOkAndHolds(EqualsProto(db1_schema)));
+
+  // Set an empty schema in a different database
+  SchemaProto db2_schema;
+  result = SchemaStore::SetSchemaResult();
+  result.success = true;
+  EXPECT_THAT(schema_store->SetSchema(
+                  db2_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/false),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+
+  // Check the schema, this should not have changed
+  EXPECT_THAT(schema_store->GetSchema(),
+              IsOkAndHolds(Pointee(EqualsProto(db1_schema))));
+  EXPECT_THAT(schema_store->GetSchema("db1"),
+              IsOkAndHolds(EqualsProto(db1_schema)));
+
+  // GetSchema for an empty database should return NotFoundError
+  EXPECT_THAT(schema_store->GetSchema("db2"),
+              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
+}
+
 TEST_F(SchemaStoreTest, SetIncompatibleSchemaOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // Set it for the first time
   SchemaStore::SetSchemaResult result;
@@ -554,10 +1306,264 @@
               IsOkAndHolds(EqualsSetSchemaResult(result)));
 }
 
+TEST_F(SchemaStoreTest, SetIncompatibleInDifferentDatabaseOk) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*enable_schema_database=*/true,
+                          /*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();
+  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(
+                  db1_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/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");
+  EXPECT_THAT(schema_store->SetSchema(
+                  db2_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/false),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+  ICING_ASSERT_OK_AND_ASSIGN(const SchemaProto* actual_full_schema,
+                             schema_store->GetSchema());
+  EXPECT_THAT(*actual_full_schema, EqualsProto(expected_full_schema));
+
+  // 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"))
+          .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_id.insert(3);  // db2_message
+  EXPECT_THAT(
+      schema_store->SetSchema(db2_schema_incompatible,
+                              /*ignore_errors_and_delete_documents=*/false,
+                              /*allow_circular_schema_definitions=*/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"),
+              IsOkAndHolds(EqualsProto(db1_schema)));
+  EXPECT_THAT(schema_store->GetSchema("db2"),
+              IsOkAndHolds(EqualsProto(db2_schema)));
+}
+
+TEST_F(SchemaStoreTest, SetInvalidInDifferentDatabaseFails) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*enable_schema_database=*/true,
+                          /*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();
+  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(
+                  db1_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/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");
+  EXPECT_THAT(schema_store->SetSchema(
+                  db2_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/false),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+  ICING_ASSERT_OK_AND_ASSIGN(const SchemaProto* actual_full_schema,
+                             schema_store->GetSchema());
+  EXPECT_THAT(*actual_full_schema, EqualsProto(expected_full_schema));
+
+  // Make db2 invalid by duplicating a property name
+  PropertyConfigProto prop =
+      PropertyConfigBuilder()
+          .SetName("prop0")
+          .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
+          .SetCardinality(CARDINALITY_OPTIONAL)
+          .Build();
+  SchemaProto db2_schema_incompatible = SchemaBuilder()
+                                            .AddType(SchemaTypeConfigBuilder()
+                                                         .SetType("db2_email")
+                                                         .SetDatabase("db2")
+                                                         .AddProperty(prop)
+                                                         .AddProperty(prop))
+                                            .Build();
+  EXPECT_THAT(
+      schema_store->SetSchema(db2_schema_incompatible,
+                              /*ignore_errors_and_delete_documents=*/false,
+                              /*allow_circular_schema_definitions=*/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"),
+              IsOkAndHolds(EqualsProto(db1_schema)));
+  EXPECT_THAT(schema_store->GetSchema("db2"),
+              IsOkAndHolds(EqualsProto(db2_schema)));
+}
+
+TEST_F(SchemaStoreTest, SetSchemaWithMultipleDbFails) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*enable_schema_database=*/true,
+                          /*initialize_stats=*/nullptr));
+
+  // Set schema for the first time
+  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(
+                  combined_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/false),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+TEST_F(SchemaStoreTest, SetSchemaWithDuplicateTypeNameAcrossDifferentDbFails) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*enable_schema_database=*/true,
+                          /*initialize_stats=*/nullptr));
+
+  // Set schema for the first time
+  SchemaProto db1_schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder().SetType("email").SetDatabase("db1"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .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");
+  EXPECT_THAT(schema_store->SetSchema(
+                  db1_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/false),
+              IsOkAndHolds(EqualsSetSchemaResult(result)));
+  EXPECT_THAT(schema_store->GetSchema(),
+              IsOkAndHolds(Pointee(EqualsProto(db1_schema))));
+  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"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2_message")
+                       .SetDatabase("db2"))
+          .Build();
+  EXPECT_THAT(schema_store->SetSchema(
+                  db2_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/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"),
+              IsOkAndHolds(EqualsProto(db1_schema)));
+}
+
 TEST_F(SchemaStoreTest, SetSchemaWithAddedTypeOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   SchemaProto schema = SchemaBuilder()
                            .AddType(SchemaTypeConfigBuilder().SetType("email"))
@@ -595,7 +1601,8 @@
 TEST_F(SchemaStoreTest, SetSchemaWithDeletedTypeOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   SchemaProto schema =
       SchemaBuilder()
@@ -658,7 +1665,8 @@
 TEST_F(SchemaStoreTest, SetSchemaWithReorderedTypesOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   SchemaProto schema =
       SchemaBuilder()
@@ -705,7 +1713,8 @@
 TEST_F(SchemaStoreTest, IndexedPropertyChangeRequiresReindexingOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   SchemaProto schema =
       SchemaBuilder()
@@ -753,7 +1762,8 @@
 TEST_F(SchemaStoreTest, IndexNestedDocumentsChangeRequiresReindexingOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // Make two schemas. One that sets index_nested_properties to false and one
   // that sets it to true.
@@ -831,7 +1841,8 @@
 TEST_F(SchemaStoreTest, SetSchemaWithIncompatibleTypesOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   SchemaProto schema =
       SchemaBuilder()
@@ -898,7 +1909,8 @@
 TEST_F(SchemaStoreTest, SetSchemaWithIncompatibleNestedTypesOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // 1. Create a ContactPoint type with a repeated property and set that schema
   SchemaTypeConfigBuilder contact_point_repeated_label =
@@ -971,7 +1983,8 @@
 TEST_F(SchemaStoreTest, SetSchemaWithIndexIncompatibleNestedTypesOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // 1. Create a ContactPoint type with label that matches prefix and set that
   // schema
@@ -1030,7 +2043,8 @@
 TEST_F(SchemaStoreTest, SetSchemaWithCompatibleNestedTypesOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // 1. Create a ContactPoint type with a optional property and set that schema
   SchemaTypeConfigBuilder contact_point_optional_label =
@@ -1088,7 +2102,8 @@
 TEST_F(SchemaStoreTest, SetSchemaWithAddedIndexableNestedTypeOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // 1. Create a ContactPoint type with a optional property, and a type that
   //    references the ContactPoint type.
@@ -1154,7 +2169,8 @@
 TEST_F(SchemaStoreTest, SetSchemaWithAddedJoinableNestedTypeOk) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // 1. Create a ContactPoint type with a optional property, and a type that
   //    references the ContactPoint type.
@@ -1166,7 +2182,7 @@
                   .SetName("label")
                   .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
                   .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                               /*propagate_delete=*/false)
+                               DELETE_PROPAGATION_TYPE_NONE)
                   .SetCardinality(CARDINALITY_REQUIRED));
   SchemaTypeConfigBuilder person =
       SchemaTypeConfigBuilder().SetType("Person").AddProperty(
@@ -1216,10 +2232,140 @@
   EXPECT_THAT(*actual_schema, EqualsProto(new_schema));
 }
 
+TEST_F(SchemaStoreTest, SetSchemaByUpdatingScorablePropertyOk) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
+
+  SchemaProto old_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("email").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("title")
+                  .SetDataType(TYPE_STRING)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+  SchemaProto new_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("title")
+                                        .SetDataType(TYPE_STRING)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("score")
+                                        .SetDataType(TYPE_DOUBLE)
+                                        .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  // Set old schema
+  SchemaStore::SetSchemaResult expected_result;
+  expected_result.success = true;
+  expected_result.schema_types_new_by_name.insert("email");
+  EXPECT_THAT(schema_store->SetSchema(
+                  old_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/false),
+              IsOkAndHolds(EqualsSetSchemaResult(expected_result)));
+  ICING_ASSERT_OK_AND_ASSIGN(const SchemaProto* actual_schema,
+                             schema_store->GetSchema());
+  EXPECT_THAT(*actual_schema, EqualsProto(old_schema));
+
+  // Set new schema.
+  // The new schema adds "score" as scorable_type ENABLED from type "email".
+  SchemaStore::SetSchemaResult new_expected_result;
+  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");
+  EXPECT_THAT(schema_store->SetSchema(
+                  new_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/false),
+              IsOkAndHolds(EqualsSetSchemaResult(new_expected_result)));
+  ICING_ASSERT_OK_AND_ASSIGN(actual_schema, schema_store->GetSchema());
+  EXPECT_THAT(*actual_schema, EqualsProto(new_schema));
+}
+
+TEST_F(SchemaStoreTest,
+       SetSchemaWithReorderedSchemeTypesAndUpdatedScorablePropertyOk) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
+
+  SchemaProto old_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("message"))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("title")
+                                        .SetDataType(TYPE_STRING)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("score")
+                                        .SetDataType(TYPE_DOUBLE)
+                                        .SetScorableType(SCORABLE_TYPE_DISABLED)
+                                        .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".
+  SchemaProto new_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("title")
+                                        .SetDataType(TYPE_STRING)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("score")
+                                        .SetDataType(TYPE_DOUBLE)
+                                        .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(SchemaTypeConfigBuilder().SetType("message"))
+          .Build();
+
+  // Set old schema
+  SchemaStore::SetSchemaResult expected_result;
+  expected_result.success = true;
+  expected_result.schema_types_new_by_name.insert("email");
+  expected_result.schema_types_new_by_name.insert("message");
+  EXPECT_THAT(schema_store->SetSchema(
+                  old_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/false),
+              IsOkAndHolds(EqualsSetSchemaResult(expected_result)));
+  ICING_ASSERT_OK_AND_ASSIGN(const SchemaProto* actual_schema,
+                             schema_store->GetSchema());
+  EXPECT_THAT(*actual_schema, EqualsProto(old_schema));
+
+  // Set new schema.
+  SchemaStore::SetSchemaResult new_expected_result;
+  new_expected_result.success = true;
+  // Schema type id of "email" is updated to 0.
+  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.old_schema_type_ids_changed.insert(1);
+  EXPECT_THAT(schema_store->SetSchema(
+                  new_schema, /*ignore_errors_and_delete_documents=*/false,
+                  /*allow_circular_schema_definitions=*/false),
+              IsOkAndHolds(EqualsSetSchemaResult(new_expected_result)));
+  ICING_ASSERT_OK_AND_ASSIGN(actual_schema, schema_store->GetSchema());
+  EXPECT_THAT(*actual_schema, EqualsProto(new_schema));
+}
+
 TEST_F(SchemaStoreTest, GetSchemaTypeId) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   schema_.clear_types();
 
@@ -1249,7 +2395,8 @@
 TEST_F(SchemaStoreTest, UpdateChecksumDefaultOnEmptySchemaStore) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   EXPECT_THAT(schema_store->GetChecksum(), IsOkAndHolds(Crc32()));
   EXPECT_THAT(schema_store->UpdateChecksum(), IsOkAndHolds(Crc32()));
@@ -1259,7 +2406,8 @@
 TEST_F(SchemaStoreTest, UpdateChecksumSameBetweenCalls) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   SchemaProto foo_schema =
       SchemaBuilder().AddType(SchemaTypeConfigBuilder().SetType("foo")).Build();
@@ -1280,7 +2428,8 @@
 TEST_F(SchemaStoreTest, UpdateChecksumSameAcrossInstances) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   SchemaProto foo_schema =
       SchemaBuilder().AddType(SchemaTypeConfigBuilder().SetType("foo")).Build();
@@ -1297,8 +2446,8 @@
   schema_store.reset();
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      schema_store, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                        &fake_clock_, feature_flags_.get()));
   EXPECT_THAT(schema_store->GetChecksum(), IsOkAndHolds(checksum));
   EXPECT_THAT(schema_store->UpdateChecksum(), IsOkAndHolds(checksum));
   EXPECT_THAT(schema_store->GetChecksum(), IsOkAndHolds(checksum));
@@ -1307,7 +2456,8 @@
 TEST_F(SchemaStoreTest, UpdateChecksumChangesOnModification) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   SchemaProto foo_schema =
       SchemaBuilder().AddType(SchemaTypeConfigBuilder().SetType("foo")).Build();
@@ -1341,7 +2491,8 @@
 TEST_F(SchemaStoreTest, PersistToDiskFineForEmptySchemaStore) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // Persisting is fine and shouldn't affect anything
   ICING_EXPECT_OK(schema_store->PersistToDisk());
@@ -1350,7 +2501,8 @@
 TEST_F(SchemaStoreTest, UpdateChecksumAvoidsRecovery) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   SchemaProto schema =
       SchemaBuilder().AddType(SchemaTypeConfigBuilder().SetType("foo")).Build();
@@ -1374,7 +2526,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store_two,
       SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
-                          &initialize_stats));
+                          feature_flags_.get(),
+                          /*enable_schema_database=*/false, &initialize_stats));
   EXPECT_THAT(initialize_stats.schema_store_recovery_cause(),
               Eq(InitializeStatsProto::NONE));
   ICING_ASSERT_OK_AND_ASSIGN(actual_schema, schema_store_two->GetSchema());
@@ -1389,7 +2542,8 @@
 TEST_F(SchemaStoreTest, PersistToDiskPreservesAcrossInstances) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   SchemaProto schema =
       SchemaBuilder().AddType(SchemaTypeConfigBuilder().SetType("foo")).Build();
@@ -1419,8 +2573,10 @@
   // And we get the same schema back on reinitialization
   InitializeStatsProto initialize_stats;
   ICING_ASSERT_OK_AND_ASSIGN(
-      schema_store, SchemaStore::Create(&filesystem_, schema_store_dir_,
-                                        &fake_clock_, &initialize_stats));
+      schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get(),
+                          /*enable_schema_database=*/false, &initialize_stats));
   EXPECT_THAT(initialize_stats.schema_store_recovery_cause(),
               Eq(InitializeStatsProto::NONE));
   ICING_ASSERT_OK_AND_ASSIGN(actual_schema, schema_store->GetSchema());
@@ -1430,7 +2586,8 @@
 TEST_F(SchemaStoreTest, SchemaStoreStorageInfoProto) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // Create a schema with two types: one simple type and one type that uses all
   // 64 sections.
@@ -1472,7 +2629,8 @@
 TEST_F(SchemaStoreTest, GetDebugInfo) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // Set schema
   ASSERT_THAT(
@@ -1493,7 +2651,8 @@
 TEST_F(SchemaStoreTest, GetDebugInfoForEmptySchemaStore) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // Check debug info before setting a schema
   ICING_ASSERT_OK_AND_ASSIGN(SchemaDebugInfoProto out,
@@ -1510,7 +2669,8 @@
   {
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     SchemaProto schema = SchemaBuilder()
                              .AddType(SchemaTypeConfigBuilder().SetType("Type"))
                              .Build();
@@ -1525,7 +2685,7 @@
       .WillByDefault(Return(false));
   {
     EXPECT_THAT(SchemaStore::Create(mock_filesystem.get(), schema_store_dir_,
-                                    &fake_clock_),
+                                    &fake_clock_, feature_flags_.get()),
                 StatusIs(libtextclassifier3::StatusCode::INTERNAL));
   }
 }
@@ -1549,7 +2709,8 @@
   {
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     SchemaProto schema = SchemaBuilder().AddType(type).Build();
     ICING_ASSERT_OK(schema_store->SetSchema(
         std::move(schema), /*ignore_errors_and_delete_documents=*/false,
@@ -1561,7 +2722,7 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
         SchemaStore::Create(mock_filesystem.get(), schema_store_dir_,
-                            &fake_clock_));
+                            &fake_clock_, feature_flags_.get()));
 
     ON_CALL(*mock_filesystem,
             CreateDirectoryRecursively(HasSubstr("key_mapper_dir")))
@@ -1584,10 +2745,12 @@
             .Build();
     SectionMetadata expected_int_prop1_metadata(
         /*id_in=*/0, TYPE_INT64, TOKENIZER_NONE, TERM_MATCH_UNKNOWN,
-        NUMERIC_MATCH_RANGE, EMBEDDING_INDEXING_UNKNOWN, "intProp1");
+        NUMERIC_MATCH_RANGE, EMBEDDING_INDEXING_UNKNOWN, QUANTIZATION_TYPE_NONE,
+        "intProp1");
     SectionMetadata expected_string_prop1_metadata(
         /*id_in=*/1, TYPE_STRING, TOKENIZER_PLAIN, TERM_MATCH_EXACT,
-        NUMERIC_MATCH_UNKNOWN, EMBEDDING_INDEXING_UNKNOWN, "stringProp1");
+        NUMERIC_MATCH_UNKNOWN, EMBEDDING_INDEXING_UNKNOWN,
+        QUANTIZATION_TYPE_NONE, "stringProp1");
     ICING_ASSERT_OK_AND_ASSIGN(SectionGroup section_group,
                                schema_store->ExtractSections(document));
     ASSERT_THAT(section_group.string_sections, SizeIs(1));
@@ -1606,7 +2769,8 @@
 TEST_F(SchemaStoreTest, CanCheckForPropertiesDefinedInSchema) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // Set it for the first time
   SchemaStore::SetSchemaResult result;
@@ -1648,7 +2812,8 @@
 TEST_F(SchemaStoreTest, GetSchemaTypeIdsWithChildren) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // Create a schema with the following inheritance relation:
   //       A
@@ -1717,7 +2882,8 @@
 TEST_F(SchemaStoreTest, DiamondGetSchemaTypeIdsWithChildren) {
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
 
   // Create a schema with the following inheritance relation:
   //       A
@@ -1805,7 +2971,7 @@
                   .SetName("senderQualifiedId")
                   .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
                   .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                               /*propagate_delete=*/true)
+                               DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
                   .SetCardinality(CARDINALITY_REQUIRED))
           .AddProperty(PropertyConfigBuilder()
                            .SetName("recipients")
@@ -1824,7 +2990,8 @@
   SchemaProto schema = SchemaBuilder().AddType(email_type).Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/true));
@@ -1851,21 +3018,22 @@
                            .SetName("tagQualifiedId")
                            .SetDataType(TYPE_STRING)
                            .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                                        /*propagate_delete=*/true)
+                                        DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
                            .SetCardinality(CARDINALITY_REQUIRED))
           .AddProperty(
               PropertyConfigBuilder()
                   .SetName("senderQualifiedId")
                   .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
                   .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                               /*propagate_delete=*/true)
+                               DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
                   .SetCardinality(CARDINALITY_REQUIRED))
           .Build();
 
   SchemaProto schema = SchemaBuilder().AddType(email_type).Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/true));
@@ -1900,7 +3068,8 @@
   SchemaProto schema = SchemaBuilder().AddType(email_type).Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/true));
@@ -1929,7 +3098,7 @@
                   .SetName("senderQualifiedId")
                   .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
                   .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                               /*propagate_delete=*/true)
+                               DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
                   .SetCardinality(CARDINALITY_REQUIRED))
           .AddProperty(PropertyConfigBuilder()
                            .SetName("timestamp")
@@ -1944,7 +3113,8 @@
   SchemaProto schema = SchemaBuilder().AddType(email_type).Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/true));
@@ -1967,7 +3137,7 @@
                            .SetName("tagQualifiedId")
                            .SetDataType(TYPE_STRING)
                            .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                                        /*propagate_delete=*/true)
+                                        DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
                            .SetCardinality(CARDINALITY_REQUIRED))
           .AddProperty(
               PropertyConfigBuilder()
@@ -2004,7 +3174,8 @@
       SchemaBuilder().AddType(email_type).AddType(conversation_type).Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/true));
@@ -2025,7 +3196,7 @@
                            .SetName("tagQualifiedId")
                            .SetDataType(TYPE_STRING)
                            .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                                        /*propagate_delete=*/true)
+                                        DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
                            .SetCardinality(CARDINALITY_REQUIRED))
           .AddProperty(
               PropertyConfigBuilder()
@@ -2062,7 +3233,8 @@
       SchemaBuilder().AddType(email_type).AddType(conversation_type).Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/true));
@@ -2083,7 +3255,7 @@
                            .SetName("tagQualifiedId")
                            .SetDataType(TYPE_STRING)
                            .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                                        /*propagate_delete=*/true)
+                                        DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
                            .SetCardinality(CARDINALITY_REQUIRED))
           .AddProperty(
               PropertyConfigBuilder()
@@ -2120,7 +3292,8 @@
       SchemaBuilder().AddType(email_type).AddType(conversation_type).Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/true));
@@ -2145,7 +3318,7 @@
                            .SetName("tagQualifiedId")
                            .SetDataType(TYPE_STRING)
                            .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                                        /*propagate_delete=*/true)
+                                        DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
                            .SetCardinality(CARDINALITY_REQUIRED))
           .AddProperty(
               PropertyConfigBuilder()
@@ -2182,7 +3355,8 @@
       SchemaBuilder().AddType(email_type).AddType(conversation_type).Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/true));
@@ -2207,7 +3381,7 @@
                            .SetName("tagQualifiedId")
                            .SetDataType(TYPE_STRING)
                            .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                                        /*propagate_delete=*/true)
+                                        DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
                            .SetCardinality(CARDINALITY_REQUIRED))
           .AddProperty(
               PropertyConfigBuilder()
@@ -2244,7 +3418,8 @@
       SchemaBuilder().AddType(email_type).AddType(conversation_type).Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/true));
@@ -2290,7 +3465,8 @@
   SchemaProto schema = SchemaBuilder().AddType(type_a).AddType(type_b).Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/true));
@@ -2360,7 +3536,8 @@
   SchemaProto schema = SchemaBuilder().AddType(type_a).AddType(type_b).Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/true));
@@ -2433,7 +3610,8 @@
   SchemaProto schema = SchemaBuilder().AddType(type_a).AddType(type_b).Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/true));
@@ -2489,7 +3667,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -2503,7 +3682,8 @@
     // present.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     EXPECT_THAT(schema_store->GetSchema(),
                 IsOkAndHolds(Pointee(EqualsProto(schema))));
 
@@ -2559,7 +3739,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -2573,7 +3754,8 @@
     // is present.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     EXPECT_THAT(schema_store->GetSchema(),
                 IsOkAndHolds(Pointee(EqualsProto(schema))));
 
@@ -2611,7 +3793,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -2627,9 +3810,9 @@
   {
     // Create a new instance of the schema store and check that it fails because
     // the backup schema is not available.
-    EXPECT_THAT(
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_),
-        StatusIs(libtextclassifier3::StatusCode::INTERNAL));
+    EXPECT_THAT(SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                    &fake_clock_, feature_flags_.get()),
+                StatusIs(libtextclassifier3::StatusCode::INTERNAL));
   }
 }
 
@@ -2661,7 +3844,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -2677,9 +3861,9 @@
   {
     // Create a new instance of the schema store and check that it fails because
     // the overlay schema is not available when we expected it to be.
-    EXPECT_THAT(
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_),
-        StatusIs(libtextclassifier3::StatusCode::INTERNAL));
+    EXPECT_THAT(SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                    &fake_clock_, feature_flags_.get()),
+                StatusIs(libtextclassifier3::StatusCode::INTERNAL));
   }
 }
 
@@ -2711,7 +3895,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -2727,9 +3912,9 @@
   {
     // Create a new of the schema store and check that the same schema is
     // present.
-    EXPECT_THAT(
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_),
-        StatusIs(libtextclassifier3::StatusCode::INTERNAL));
+    EXPECT_THAT(SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                    &fake_clock_, feature_flags_.get()),
+                StatusIs(libtextclassifier3::StatusCode::INTERNAL));
   }
 }
 
@@ -2760,7 +3945,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -2777,9 +3963,9 @@
     // Create a new instance of the schema store and check that it fails because
     // the schema header (which is now a part of the ground truth) is not
     // available.
-    EXPECT_THAT(
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_),
-        StatusIs(libtextclassifier3::StatusCode::INTERNAL));
+    EXPECT_THAT(SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                    &fake_clock_, feature_flags_.get()),
+                StatusIs(libtextclassifier3::StatusCode::INTERNAL));
   }
 }
 
@@ -2801,7 +3987,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -2812,14 +3999,15 @@
 
   ICING_EXPECT_OK(SchemaStore::MigrateSchema(
       &filesystem_, schema_store_dir_, version_util::StateChange::kCompatible,
-      version_util::kVersion));
+      version_util::kVersion, /*perform_schema_database_migration=*/false));
 
   {
     // Create a new of the schema store and check that the same schema is
     // present.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     EXPECT_THAT(schema_store->GetSchema(),
                 IsOkAndHolds(Pointee(EqualsProto(schema))));
   }
@@ -2843,7 +4031,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -2854,14 +4043,15 @@
 
   ICING_EXPECT_OK(SchemaStore::MigrateSchema(
       &filesystem_, schema_store_dir_, version_util::StateChange::kUpgrade,
-      version_util::kVersion + 1));
+      version_util::kVersion + 1, /*perform_schema_database_migration=*/false));
 
   {
     // Create a new of the schema store and check that the same schema is
     // present.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     EXPECT_THAT(schema_store->GetSchema(),
                 IsOkAndHolds(Pointee(EqualsProto(schema))));
   }
@@ -2885,7 +4075,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -2894,17 +4085,18 @@
                 IsOkAndHolds(Pointee(EqualsProto(schema))));
   }
 
-  ICING_EXPECT_OK(
-      SchemaStore::MigrateSchema(&filesystem_, schema_store_dir_,
-                                 version_util::StateChange::kVersionZeroUpgrade,
-                                 version_util::kVersion + 1));
+  ICING_EXPECT_OK(SchemaStore::MigrateSchema(
+      &filesystem_, schema_store_dir_,
+      version_util::StateChange::kVersionZeroUpgrade,
+      version_util::kVersion + 1, /*perform_schema_database_migration=*/false));
 
   {
     // Create a new of the schema store and check that the same schema is
     // present.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     EXPECT_THAT(schema_store->GetSchema(),
                 IsOkAndHolds(Pointee(EqualsProto(schema))));
   }
@@ -2929,7 +4121,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -2943,14 +4136,16 @@
   // So kVersionOne - 1 is incompatible and will throw out the schema.
   ICING_EXPECT_OK(SchemaStore::MigrateSchema(
       &filesystem_, schema_store_dir_, version_util::StateChange::kRollBack,
-      version_util::kVersionOne - 1));
+      version_util::kVersionOne - 1,
+      /*perform_schema_database_migration=*/false));
 
   {
     // Create a new of the schema store and check that we fell back to the
     // base schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
 
     SchemaTypeConfigProto other_type_a =
         SchemaTypeConfigBuilder()
@@ -2984,7 +4179,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -2998,14 +4194,15 @@
   // compatible and retain the overlay schema.
   ICING_EXPECT_OK(SchemaStore::MigrateSchema(
       &filesystem_, schema_store_dir_, version_util::StateChange::kRollBack,
-      version_util::kVersion));
+      version_util::kVersion, /*perform_schema_database_migration=*/false));
 
   {
     // Create a new of the schema store and check that the same schema is
     // present.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     EXPECT_THAT(schema_store->GetSchema(),
                 IsOkAndHolds(Pointee(EqualsProto(schema))));
   }
@@ -3026,7 +4223,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -3040,7 +4238,8 @@
   // So kVersionOne - 1 is incompatible and will throw out the schema.
   ICING_EXPECT_OK(SchemaStore::MigrateSchema(
       &filesystem_, schema_store_dir_, version_util::StateChange::kRollBack,
-      version_util::kVersionOne - 1));
+      version_util::kVersionOne - 1,
+      /*perform_schema_database_migration=*/false));
 
   SchemaTypeConfigProto other_type_a =
       SchemaTypeConfigBuilder()
@@ -3057,7 +4256,8 @@
     // base schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
 
     EXPECT_THAT(schema_store->GetSchema(),
                 IsOkAndHolds(Pointee(EqualsProto(base_schema))));
@@ -3067,13 +4267,14 @@
   // present (currently base schema)
   ICING_EXPECT_OK(SchemaStore::MigrateSchema(
       &filesystem_, schema_store_dir_, version_util::StateChange::kRollForward,
-      version_util::kVersion));
+      version_util::kVersion, /*perform_schema_database_migration=*/false));
   {
     // Create a new of the schema store and check that we fell back to the
     // base schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
 
     EXPECT_THAT(schema_store->GetSchema(),
                 IsOkAndHolds(Pointee(EqualsProto(base_schema))));
@@ -3095,7 +4296,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -3109,14 +4311,15 @@
   // compatible and retain the overlay schema.
   ICING_EXPECT_OK(SchemaStore::MigrateSchema(
       &filesystem_, schema_store_dir_, version_util::StateChange::kRollBack,
-      version_util::kVersion));
+      version_util::kVersion, /*perform_schema_database_migration=*/false));
 
   {
     // Create a new of the schema store and check that the same schema is
     // present.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
 
     EXPECT_THAT(schema_store->GetSchema(),
                 IsOkAndHolds(Pointee(EqualsProto(schema))));
@@ -3126,13 +4329,14 @@
   // present (currently overlay schema)
   ICING_EXPECT_OK(SchemaStore::MigrateSchema(
       &filesystem_, schema_store_dir_, version_util::StateChange::kRollForward,
-      version_util::kVersion));
+      version_util::kVersion, /*perform_schema_database_migration=*/false));
   {
     // Create a new of the schema store and check that the same schema is
     // present.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
 
     EXPECT_THAT(schema_store->GetSchema(),
                 IsOkAndHolds(Pointee(EqualsProto(schema))));
@@ -3155,7 +4359,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -3169,7 +4374,7 @@
   ICING_EXPECT_OK(SchemaStore::MigrateSchema(
       &filesystem_, schema_store_dir_,
       version_util::StateChange::kVersionZeroRollForward,
-      version_util::kVersion));
+      version_util::kVersion, /*perform_schema_database_migration=*/false));
 
   SchemaTypeConfigProto other_type_a =
       SchemaTypeConfigBuilder()
@@ -3186,7 +4391,8 @@
     // base schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
 
     EXPECT_THAT(schema_store->GetSchema(),
                 IsOkAndHolds(Pointee(EqualsProto(base_schema))));
@@ -3208,7 +4414,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -3222,7 +4429,7 @@
   // it should always be valid.
   ICING_EXPECT_OK(SchemaStore::MigrateSchema(
       &filesystem_, schema_store_dir_, version_util::StateChange::kUndetermined,
-      version_util::kVersion));
+      version_util::kVersion, /*perform_schema_database_migration=*/false));
 
   SchemaTypeConfigProto other_type_a =
       SchemaTypeConfigBuilder()
@@ -3239,7 +4446,8 @@
     // base schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
 
     EXPECT_THAT(schema_store->GetSchema(),
                 IsOkAndHolds(Pointee(EqualsProto(base_schema))));
@@ -3296,7 +4504,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -3367,7 +4576,8 @@
     // Create an instance of the schema store and set the schema.
     ICING_ASSERT_OK_AND_ASSIGN(
         std::unique_ptr<SchemaStore> schema_store,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
     ICING_ASSERT_OK(schema_store->SetSchema(
         schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
@@ -3381,6 +4591,501 @@
   }
 }
 
+TEST_F(SchemaStoreTest, GetScorablePropertyIndex_SchemaNotSet) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
+
+  EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                  /*schema_type_id=*/0,
+                  /*property_path=*/"timestamp"),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+}
+
+TEST_F(SchemaStoreTest, GetScorablePropertyIndex_InvalidSchemaTypeId) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
+
+  // Set schema
+  ICING_ASSERT_OK(schema_store->SetSchema(
+      schema_, /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/false));
+
+  // non-existing schema type id
+  EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                  /*schema_type_id=*/100,
+                  /*property_path=*/"timestamp"),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+TEST_F(SchemaStoreTest, GetScorablePropertyIndex_InvalidPropertyName) {
+  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")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("scoreDouble")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetCardinality(CARDINALITY_OPTIONAL)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("timestamp")
+                                   .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)
+                                   .SetScorableType(SCORABLE_TYPE_ENABLED)))
+          .Build();
+
+  // Set schema
+  ICING_ASSERT_OK(schema_store->SetSchema(
+      schema, /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/false));
+
+  // non-scorable property
+  EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                  /*schema_type_id=*/0,
+                  /*property_path=*/"subject"),
+              IsOkAndHolds(Eq(std::nullopt)));
+  // non-existing property
+  EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                  /*schema_type_id=*/0,
+                  /*property_path=*/"non_existing"),
+              IsOkAndHolds(Eq(std::nullopt)));
+}
+
+TEST_F(SchemaStoreTest, GetScorablePropertyIndex_Ok) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
+
+  // Set schema
+  ICING_ASSERT_OK(schema_store->SetSchema(
+      schema_, /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/false));
+
+  EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                  /*schema_type_id=*/0,
+                  /*property_path=*/"timestamp"),
+              IsOkAndHolds(0));
+}
+
+TEST_F(SchemaStoreTest, GetOrderedScorablePropertyPaths_SchemaNotSet) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
+
+  EXPECT_THAT(schema_store->GetOrderedScorablePropertyInfo(
+                  /*schema_type_id=*/0),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+}
+
+TEST_F(SchemaStoreTest, GetOrderedScorablePropertyPaths_InvalidSchemaTypeId) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
+
+  // Set schema
+  ICING_ASSERT_OK(schema_store->SetSchema(
+      schema_, /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/false));
+
+  EXPECT_THAT(schema_store->GetOrderedScorablePropertyInfo(
+                  /*schema_type_id=*/100),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+TEST_F(SchemaStoreTest, GetOrderedScorablePropertyPaths_Ok) {
+  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")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("scoreDouble")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetCardinality(CARDINALITY_OPTIONAL)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("timestamp")
+                                   .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)
+                                   .SetScorableType(SCORABLE_TYPE_ENABLED)))
+          .AddType(SchemaTypeConfigBuilder().SetType("message").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("subject")
+                  .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  // Set schema
+  ICING_ASSERT_OK(schema_store->SetSchema(
+      schema, /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/false));
+  EXPECT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(0));
+  EXPECT_THAT(schema_store->GetSchemaTypeId("message"), IsOkAndHolds(1));
+
+  // no scorable properties under the schema, 'message'.
+  EXPECT_THAT(schema_store->GetOrderedScorablePropertyInfo(
+                  /*schema_type_id=*/1),
+              IsOkAndHolds(Pointee(ElementsAre())));
+
+  EXPECT_THAT(schema_store->GetOrderedScorablePropertyInfo(
+                  /*schema_type_id=*/0),
+              IsOkAndHolds(Pointee(ElementsAre(
+                  EqualsScorablePropertyInfo("scoreDouble", TYPE_DOUBLE),
+                  EqualsScorablePropertyInfo("timestamp", TYPE_INT64)))));
+}
+
+TEST_F(SchemaStoreTest, ScorablePropertyManagerUpdatesUponSchemaChange) {
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<SchemaStore> schema_store,
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
+
+  // Sets the initial schema
+  ICING_ASSERT_OK(schema_store->SetSchema(
+      schema_, /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/false));
+
+  EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                  /*schema_type_id=*/0,
+                  /*property_path=*/"timestamp"),
+              IsOkAndHolds(0));
+  EXPECT_THAT(schema_store->GetOrderedScorablePropertyInfo(
+                  /*schema_type_id=*/0),
+              IsOkAndHolds(Pointee(ElementsAre(
+                  EqualsScorablePropertyInfo("timestamp", TYPE_INT64)))));
+
+  // The new schema drops the type 'email', and adds a new type 'message'.
+  SchemaProto new_schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("message")
+                  .AddProperty(
+                      // Add an indexed property so we generate
+                      // section metadata on it
+                      PropertyConfigBuilder()
+                          .SetName("content")
+                          .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("scoreInt")
+                                   .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                   .SetCardinality(CARDINALITY_OPTIONAL)
+                                   .SetScorableType(SCORABLE_TYPE_ENABLED))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("scoreDouble")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetCardinality(CARDINALITY_OPTIONAL)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)))
+          .Build();
+
+  // Force updates the schema.
+  ICING_ASSERT_OK(schema_store->SetSchema(
+      new_schema, /*ignore_errors_and_delete_documents=*/true,
+      /*allow_circular_schema_definitions=*/false));
+
+  // "timestamp" is no longer a valid property name.
+  EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                  /*schema_type_id=*/0,
+                  /*property_path=*/"timestamp"),
+              IsOkAndHolds(Eq(std::nullopt)));
+
+  // ok cases for the new schema.
+  EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                  /*schema_type_id=*/0,
+                  /*property_path=*/"scoreInt"),
+              IsOkAndHolds(1));
+  EXPECT_THAT(schema_store->GetScorablePropertyIndex(
+                  /*schema_type_id=*/0,
+                  /*property_path=*/"scoreDouble"),
+              IsOkAndHolds(0));
+  EXPECT_THAT(schema_store->GetOrderedScorablePropertyInfo(
+                  /*schema_type_id=*/0),
+              IsOkAndHolds(Pointee(ElementsAre(
+                  EqualsScorablePropertyInfo("scoreDouble", TYPE_DOUBLE),
+                  EqualsScorablePropertyInfo("scoreInt", TYPE_INT64)))));
+}
+
+class SchemaStoreTestWithParam
+    : public SchemaStoreTest,
+      public testing::WithParamInterface<version_util::StateChange> {};
+
+TEST_P(SchemaStoreTestWithParam, MigrateSchemaWithDatabaseMigration) {
+  SchemaProto schema_no_database =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db1/email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("db1Subject")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_RFC822)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("db2/email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("db2Subject")
+                                        .SetDataTypeString(TERM_MATCH_EXACT,
+                                                           TOKENIZER_PLAIN)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  {
+    // Create an instance of the schema store and set the schema.
+    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(
+        schema_no_database, /*ignore_errors_and_delete_documents=*/false,
+        /*allow_circular_schema_definitions=*/false));
+
+    EXPECT_THAT(schema_store->GetSchema(),
+                IsOkAndHolds(Pointee(EqualsProto(schema_no_database))));
+  }
+
+  SchemaTypeConfigProto db1_email_rfc =
+      SchemaTypeConfigBuilder()
+          .SetType("db1/email")
+          .SetDatabase("db1")
+          .AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("db1Subject")
+                  .SetCardinality(CARDINALITY_OPTIONAL)
+                  .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_RFC822))
+          .Build();
+  SchemaTypeConfigProto db1_email_no_rfc =
+      SchemaTypeConfigBuilder()
+          .SetType("db1/email")
+          .SetDatabase("db1")
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("db1Subject")
+                           .SetCardinality(CARDINALITY_OPTIONAL)
+                           .SetDataType(TYPE_STRING))
+          .Build();
+  SchemaTypeConfigProto db2_email =
+      SchemaTypeConfigBuilder()
+          .SetType("db2/email")
+          .SetDatabase("db2")
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("db2Subject")
+                           .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                           .SetCardinality(CARDINALITY_OPTIONAL))
+          .Build();
+  SchemaProto full_schema_with_database_rfc =
+      SchemaBuilder().AddType(db1_email_rfc).AddType(db2_email).Build();
+  SchemaProto full_schema_with_database_no_rfc =
+      SchemaBuilder().AddType(db1_email_no_rfc).AddType(db2_email).Build();
+  SchemaProto db1_schema_rfc = SchemaBuilder().AddType(db1_email_rfc).Build();
+  SchemaProto db1_schema_no_rfc =
+      SchemaBuilder().AddType(db1_email_no_rfc).Build();
+  SchemaProto db2_schema = SchemaBuilder().AddType(db2_email).Build();
+
+  ICING_EXPECT_OK(SchemaStore::MigrateSchema(
+      &filesystem_, schema_store_dir_, /*version_state_change=*/GetParam(),
+      version_util::kVersion, /*perform_schema_database_migration=*/true));
+  {
+    // Create a new instance of the schema store and check that the database
+    // field is populated.
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<SchemaStore> schema_store,
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
+
+    if (GetParam() == version_util::StateChange::kVersionZeroRollForward ||
+        GetParam() == version_util::StateChange::kUndetermined) {
+      // For these cases, the overlay schema is discarded and we fall back to
+      // the backup schema (no rfc version).
+      EXPECT_THAT(
+          schema_store->GetSchema(),
+          IsOkAndHolds(Pointee(EqualsProto(full_schema_with_database_no_rfc))));
+      EXPECT_THAT(schema_store->GetSchema("db1"),
+                  IsOkAndHolds(EqualsProto(db1_schema_no_rfc)));
+      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"),
+                  IsOkAndHolds(EqualsProto(db1_schema_rfc)));
+      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")
+              .Build();
+      DocumentProto db2_email_doc =
+          DocumentBuilder()
+              .SetKey("namespace", "uri3")
+              .SetSchema("db2/email")
+              .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"));
+      ICING_ASSERT_OK_AND_ASSIGN(section_group,
+                                 schema_store->ExtractSections(db2_email_doc));
+      EXPECT_THAT(section_group.string_sections[0].content,
+                  ElementsAre("db2_subject"));
+
+      // Verify that our persisted data are ok
+      EXPECT_THAT(schema_store->GetSchemaTypeId("db1/email"), IsOkAndHolds(0));
+      EXPECT_THAT(schema_store->GetSchemaTypeId("db2/email"), IsOkAndHolds(1));
+    }
+  }
+}
+
+TEST_P(SchemaStoreTestWithParam,
+       MigrateSchemaWithDatabaseMigration_noDbPrefix) {
+  SchemaTypeConfigProto email_rfc =
+      SchemaTypeConfigBuilder()
+          .SetType("email")
+          .AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("subject")
+                  .SetCardinality(CARDINALITY_OPTIONAL)
+                  .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_RFC822))
+          .Build();
+  SchemaTypeConfigProto email_no_rfc =
+      SchemaTypeConfigBuilder()
+          .SetType("email")
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("subject")
+                           .SetCardinality(CARDINALITY_OPTIONAL)
+                           .SetDataType(TYPE_STRING))
+          .Build();
+  SchemaTypeConfigProto message =
+      SchemaTypeConfigBuilder()
+          .SetType("message")
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("content")
+                           .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                           .SetCardinality(CARDINALITY_OPTIONAL))
+          .Build();
+
+  SchemaProto original_schema =
+      SchemaBuilder().AddType(email_rfc).AddType(message).Build();
+
+  {
+    // Create an instance of the schema store and set the schema.
+    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(
+        original_schema, /*ignore_errors_and_delete_documents=*/false,
+        /*allow_circular_schema_definitions=*/false));
+
+    EXPECT_THAT(schema_store->GetSchema(),
+                IsOkAndHolds(Pointee(EqualsProto(original_schema))));
+  }
+
+  SchemaProto backup_schema =
+      SchemaBuilder().AddType(email_no_rfc).AddType(message).Build();
+
+  ICING_EXPECT_OK(SchemaStore::MigrateSchema(
+      &filesystem_, schema_store_dir_, /*version_state_change=*/GetParam(),
+      version_util::kVersion, /*perform_schema_database_migration=*/true));
+
+  {
+    // Create a new instance of the schema store and check that the database
+    // field is populated.
+    ICING_ASSERT_OK_AND_ASSIGN(
+        std::unique_ptr<SchemaStore> schema_store,
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                            feature_flags_.get()));
+
+    if (GetParam() == version_util::StateChange::kVersionZeroRollForward ||
+        GetParam() == version_util::StateChange::kUndetermined) {
+      // For these cases, the overlay schema is discarded and we fall back to
+      // the backup schema (no rfc version).
+      EXPECT_THAT(schema_store->GetSchema(),
+                  IsOkAndHolds(Pointee(EqualsProto(backup_schema))));
+      EXPECT_THAT(schema_store->GetSchema("db"),
+                  StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
+    } else {
+      EXPECT_THAT(schema_store->GetSchema(),
+                  IsOkAndHolds(Pointee(EqualsProto(original_schema))));
+      EXPECT_THAT(schema_store->GetSchema("db"),
+                  StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
+
+      DocumentProto email_doc = DocumentBuilder()
+                                    .SetKey("namespace", "uri1")
+                                    .SetSchema("email")
+                                    .AddStringProperty("subject", "subject")
+                                    .Build();
+      DocumentProto message_doc = DocumentBuilder()
+                                      .SetKey("namespace", "uri2")
+                                      .SetSchema("message")
+                                      .AddStringProperty("content", "content")
+                                      .Build();
+
+      // Verify that our in-memory structures are ok
+      ICING_ASSERT_OK_AND_ASSIGN(SectionGroup section_group,
+                                 schema_store->ExtractSections(email_doc));
+      EXPECT_THAT(section_group.string_sections[0].content,
+                  ElementsAre("subject"));
+      ICING_ASSERT_OK_AND_ASSIGN(section_group,
+                                 schema_store->ExtractSections(message_doc));
+      EXPECT_THAT(section_group.string_sections[0].content,
+                  ElementsAre("content"));
+
+      // Verify that our persisted data are ok
+      EXPECT_THAT(schema_store->GetSchemaTypeId("email"), IsOkAndHolds(0));
+      EXPECT_THAT(schema_store->GetSchemaTypeId("message"), IsOkAndHolds(1));
+    }
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    SchemaStoreTestWithParam, SchemaStoreTestWithParam,
+    testing::Values(
+        /*version_state_change=*/
+        version_util::StateChange::kUndetermined,
+        version_util::StateChange::kCompatible,
+        version_util::StateChange::kRollForward,
+        version_util::StateChange::kRollBack,
+        version_util::StateChange::kUpgrade,
+        version_util::StateChange::kVersionZeroUpgrade,
+        version_util::StateChange::kVersionZeroRollForward));
+
 }  // namespace
 
 }  // namespace lib
diff --git a/icing/schema/schema-type-manager_test.cc b/icing/schema/schema-type-manager_test.cc
index eafc612..df1c493 100644
--- a/icing/schema/schema-type-manager_test.cc
+++ b/icing/schema/schema-type-manager_test.cc
@@ -89,7 +89,8 @@
   return PropertyConfigBuilder()
       .SetName(kPropertySenderQualifiedId)
       .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
-      .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID, /*propagate_delete=*/true)
+      .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                   DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
       .SetCardinality(CARDINALITY_REQUIRED)
       .Build();
 }
@@ -114,7 +115,8 @@
   return PropertyConfigBuilder()
       .SetName(kPropertyTagQualifiedId)
       .SetDataType(TYPE_STRING)
-      .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID, /*propagate_delete=*/true)
+      .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                   DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
       .SetCardinality(CARDINALITY_REQUIRED)
       .Build();
 }
@@ -123,7 +125,8 @@
   return PropertyConfigBuilder()
       .SetName(kPropertyGroupQualifiedId)
       .SetDataTypeString(TERM_MATCH_PREFIX, TOKENIZER_PLAIN)
-      .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID, /*propagate_delete=*/true)
+      .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                   DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
       .SetCardinality(CARDINALITY_REQUIRED)
       .Build();
 }
@@ -132,7 +135,8 @@
   return PropertyConfigBuilder()
       .SetName(kPropertySuperTagQualifiedId)
       .SetDataType(TYPE_STRING)
-      .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID, /*propagate_delete=*/true)
+      .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                   DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
       .SetCardinality(CARDINALITY_REQUIRED)
       .Build();
 }
diff --git a/icing/schema/schema-util.cc b/icing/schema/schema-util.cc
index cd591bc..22ce523 100644
--- a/icing/schema/schema-util.cc
+++ b/icing/schema/schema-util.cc
@@ -15,7 +15,7 @@
 #include "icing/schema/schema-util.h"
 
 #include <algorithm>
-#include <cstdint>
+#include <cctype>
 #include <queue>
 #include <string>
 #include <string_view>
@@ -25,10 +25,12 @@
 #include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/annotate.h"
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/absl_ports/str_cat.h"
 #include "icing/absl_ports/str_join.h"
+#include "icing/feature-flags.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/proto/term.pb.h"
 #include "icing/util/logging.h"
@@ -39,18 +41,58 @@
 
 namespace {
 
+bool AreStringIndexingConfigsEqual(const StringIndexingConfig& old_config,
+                                   const StringIndexingConfig& new_config) {
+  return old_config.term_match_type() == new_config.term_match_type() &&
+         old_config.tokenizer_type() == new_config.tokenizer_type();
+}
+
+bool AreDocumentIndexingConfigsEqual(const DocumentIndexingConfig& old_config,
+                                     const DocumentIndexingConfig& new_config) {
+  return old_config.index_nested_properties() ==
+         new_config.index_nested_properties();
+}
+
+bool AreIntegerIndexingConfigsEqual(const IntegerIndexingConfig& old_config,
+                                    const IntegerIndexingConfig& new_config) {
+  return old_config.numeric_match_type() == new_config.numeric_match_type();
+}
+
+bool AreJoinableConfigsEqual(const JoinableConfig& old_config,
+                             const JoinableConfig& new_config) {
+  return old_config.value_type() == new_config.value_type() &&
+         old_config.delete_propagation_type() ==
+             new_config.delete_propagation_type();
+}
+
+bool AreEmbeddingIndexingConfigsEqual(
+    const EmbeddingIndexingConfig& old_config,
+    const EmbeddingIndexingConfig& new_config) {
+  return old_config.embedding_indexing_type() ==
+             new_config.embedding_indexing_type() &&
+         old_config.quantization_type() == new_config.quantization_type();
+}
+
 bool ArePropertiesEqual(const PropertyConfigProto& old_property,
                         const PropertyConfigProto& new_property) {
   return old_property.property_name() == new_property.property_name() &&
          old_property.data_type() == new_property.data_type() &&
          old_property.schema_type() == new_property.schema_type() &&
          old_property.cardinality() == new_property.cardinality() &&
-         old_property.string_indexing_config().term_match_type() ==
-             new_property.string_indexing_config().term_match_type() &&
-         old_property.string_indexing_config().tokenizer_type() ==
-             new_property.string_indexing_config().tokenizer_type() &&
-         old_property.document_indexing_config().index_nested_properties() ==
-             new_property.document_indexing_config().index_nested_properties();
+         old_property.scorable_type() == new_property.scorable_type() &&
+         AreStringIndexingConfigsEqual(old_property.string_indexing_config(),
+                                       new_property.string_indexing_config()) &&
+         AreDocumentIndexingConfigsEqual(
+             old_property.document_indexing_config(),
+             new_property.document_indexing_config()) &&
+         AreIntegerIndexingConfigsEqual(
+             old_property.integer_indexing_config(),
+             new_property.integer_indexing_config()) &&
+         AreJoinableConfigsEqual(old_property.joinable_config(),
+                                 new_property.joinable_config()) &&
+         AreEmbeddingIndexingConfigsEqual(
+             old_property.embedding_indexing_config(),
+             new_property.embedding_indexing_config());
 }
 
 bool IsCardinalityCompatible(const PropertyConfigProto& old_property,
@@ -118,7 +160,8 @@
 bool IsEmbeddingIndexingCompatible(const EmbeddingIndexingConfig& old_indexed,
                                    const EmbeddingIndexingConfig& new_indexed) {
   return old_indexed.embedding_indexing_type() ==
-         new_indexed.embedding_indexing_type();
+             new_indexed.embedding_indexing_type() &&
+         old_indexed.quantization_type() == new_indexed.quantization_type();
 }
 
 bool IsDocumentIndexingCompatible(const DocumentIndexingConfig& old_indexed,
@@ -207,6 +250,90 @@
   return true;
 }
 
+// Builds a map of {schema_type -> set of scorable property names}
+std::unordered_map<std::string_view, std::unordered_set<std::string_view>>
+BuildTypeToScorablePropertyNamesMap(
+    const SchemaUtil::TypeConfigMap& type_config_map) {
+  std::unordered_map<std::string_view, std::unordered_set<std::string_view>>
+      type_to_scorable_property_names_map;
+  for (const auto& [schema_type, schema_type_config] : type_config_map) {
+    for (const PropertyConfigProto& property_config :
+         schema_type_config.properties()) {
+      if (property_config.scorable_type() ==
+          PropertyConfigProto::ScorableType::ENABLED) {
+        type_to_scorable_property_names_map[schema_type].insert(
+            property_config.property_name());
+      }
+    }
+  }
+  return type_to_scorable_property_names_map;
+}
+
+// Finds the schema types that have inconsistent scorable properties, which will
+// be added in place in the `schema_delta`.
+void FindScorablePropertyInconsistentTypes(
+    const SchemaUtil::TypeConfigMap& old_type_config_map,
+    const SchemaUtil::TypeConfigMap& new_type_config_map,
+    const SchemaUtil::DependentMap& new_schema_dependent_map,
+    SchemaUtil::SchemaDelta* schema_delta) {
+  std::unordered_map<std::string_view, std::unordered_set<std::string_view>>
+      new_type_to_scorable_property_names_map =
+          BuildTypeToScorablePropertyNamesMap(new_type_config_map);
+  std::unordered_map<std::string_view, std::unordered_set<std::string_view>>
+      old_type_to_scorable_property_names_map =
+          BuildTypeToScorablePropertyNamesMap(old_type_config_map);
+  for (const auto& [schema_type, _] : old_type_config_map) {
+    if (new_type_config_map.find(schema_type) == new_type_config_map.end()) {
+      // The type has been deleted in the new schema.
+      continue;
+    }
+    auto old_schema_type_property_names_iter =
+        old_type_to_scorable_property_names_map.find(schema_type);
+    auto new_schema_type_property_names_iter =
+        new_type_to_scorable_property_names_map.find(schema_type);
+    bool has_scorable_properties_in_old_schema =
+        old_schema_type_property_names_iter !=
+        old_type_to_scorable_property_names_map.end();
+    bool has_scorable_properties_in_new_schema =
+        new_schema_type_property_names_iter !=
+        new_type_to_scorable_property_names_map.end();
+    if (has_scorable_properties_in_old_schema &&
+        !has_scorable_properties_in_new_schema) {
+      schema_delta->schema_types_scorable_property_inconsistent.insert(
+          schema_type);
+    } else if (!has_scorable_properties_in_old_schema &&
+               has_scorable_properties_in_new_schema) {
+      schema_delta->schema_types_scorable_property_inconsistent.insert(
+          schema_type);
+    } else if (has_scorable_properties_in_old_schema &&
+               has_scorable_properties_in_new_schema) {
+      // The sets of scorable properties from the old and new schema are
+      // different.
+      if (old_schema_type_property_names_iter->second !=
+          new_schema_type_property_names_iter->second) {
+        schema_delta->schema_types_scorable_property_inconsistent.insert(
+            schema_type);
+      }
+    }
+  }
+
+  // Now, look up the DependentMap of the new schema config and find the parent
+  // types that depend on the currently discovered inconsistent types.
+  std::vector<std::string_view> parent_types;
+  for (const std::string& schema_type :
+       schema_delta->schema_types_scorable_property_inconsistent) {
+    auto parent_type_maps_iter = new_schema_dependent_map.find(schema_type);
+    if (parent_type_maps_iter == new_schema_dependent_map.end()) {
+      continue;
+    }
+    for (const auto& [parent_type, _] : parent_type_maps_iter->second) {
+      parent_types.push_back(parent_type);
+    }
+  }
+  schema_delta->schema_types_scorable_property_inconsistent.insert(
+      parent_types.begin(), parent_types.end());
+}
+
 }  // namespace
 
 libtextclassifier3::Status CalculateTransitiveNestedTypeRelations(
@@ -562,7 +689,8 @@
 }
 
 libtextclassifier3::StatusOr<SchemaUtil::DependentMap> SchemaUtil::Validate(
-    const SchemaProto& schema, bool allow_circular_schema_definitions) {
+    const SchemaProto& schema, const FeatureFlags& feature_flags,
+    bool allow_circular_schema_definitions) {
   // 1. Build the dependent map. This will detect any cycles, non-existent or
   // duplicate types in the schema.
   ICING_ASSIGN_OR_RETURN(
@@ -620,6 +748,10 @@
 
       ICING_RETURN_IF_ERROR(ValidateCardinality(property_config.cardinality(),
                                                 schema_type, property_name));
+      if (feature_flags.enable_scorable_properties()) {
+        ICING_RETURN_IF_ERROR(
+            ValidateScorableType(schema_type, property_config));
+      }
 
       if (data_type == PropertyConfigProto::DataType::STRING) {
         ICING_RETURN_IF_ERROR(ValidateStringIndexingConfig(
@@ -627,9 +759,10 @@
             property_name));
       }
 
-      ICING_RETURN_IF_ERROR(ValidateJoinableConfig(
-          property_config.joinable_config(), data_type,
-          property_config.cardinality(), schema_type, property_name));
+      ICING_RETURN_IF_ERROR(
+          ValidateJoinableConfig(property_config.joinable_config(), data_type,
+                                 property_config.cardinality(), schema_type,
+                                 property_name, feature_flags));
       if (property_config.joinable_config().value_type() !=
           JoinableConfig::ValueType::NONE) {
         schema_types_with_joinable_property.insert(schema_type);
@@ -744,6 +877,42 @@
   return libtextclassifier3::Status::OK;
 }
 
+libtextclassifier3::Status SchemaUtil::ValidateScorableType(
+    std::string_view schema_type,
+    const PropertyConfigProto& property_config_proto) {
+  if (property_config_proto.data_type() ==
+      PropertyConfigProto::DataType::DOCUMENT) {
+    if (property_config_proto.scorable_type() !=
+        PropertyConfigProto::ScorableType::UNKNOWN) {
+      return absl_ports::InvalidArgumentError(absl_ports::StrCat(
+          "Field 'scorable_type' shouldn't be explicitly set for data type "
+          "DOCUMENT. It is considered scorable if any of its or its "
+          "dependency's property is scorable."));
+    }
+  }
+
+  if (property_config_proto.scorable_type() ==
+          PropertyConfigProto::ScorableType::DISABLED ||
+      property_config_proto.scorable_type() ==
+          PropertyConfigProto::ScorableType::UNKNOWN) {
+    return libtextclassifier3::Status::OK;
+  }
+
+  switch (property_config_proto.data_type()) {
+    case PropertyConfigProto::DataType::INT64:
+    case PropertyConfigProto::DataType::DOUBLE:
+    case PropertyConfigProto::DataType::BOOLEAN:
+      return libtextclassifier3::Status::OK;
+    default:
+      return absl_ports::InvalidArgumentError(absl_ports::StrCat(
+          "Field 'scorable_type' cannot be enabled for data type '",
+          PropertyConfigProto::DataType::Code_Name(
+              property_config_proto.data_type()),
+          "' for schema property '", schema_type, ".",
+          property_config_proto.property_name(), "'"));
+  }
+}
+
 libtextclassifier3::Status SchemaUtil::ValidateStringIndexingConfig(
     const StringIndexingConfig& config,
     PropertyConfigProto::DataType::Code data_type, std::string_view schema_type,
@@ -770,7 +939,8 @@
 libtextclassifier3::Status SchemaUtil::ValidateJoinableConfig(
     const JoinableConfig& config, PropertyConfigProto::DataType::Code data_type,
     PropertyConfigProto::Cardinality::Code cardinality,
-    std::string_view schema_type, std::string_view property_name) {
+    std::string_view schema_type, std::string_view property_name,
+    const FeatureFlags& feature_flags) {
   if (config.value_type() == JoinableConfig::ValueType::QUALIFIED_ID) {
     if (data_type != PropertyConfigProto::DataType::STRING) {
       return absl_ports::InvalidArgumentError(
@@ -778,14 +948,16 @@
                              "' is required to have STRING data type"));
     }
 
-    if (cardinality == PropertyConfigProto::Cardinality::REPEATED) {
+    if (!feature_flags.enable_repeated_field_joins() &&
+        cardinality == PropertyConfigProto::Cardinality::REPEATED) {
       return absl_ports::InvalidArgumentError(
           absl_ports::StrCat("Qualified id joinable property '", property_name,
                              "' cannot have REPEATED cardinality"));
     }
   }
 
-  if (config.propagate_delete() &&
+  if (config.delete_propagation_type() !=
+          JoinableConfig::DeletePropagationType::NONE &&
       config.value_type() != JoinableConfig::ValueType::QUALIFIED_ID) {
     return absl_ports::InvalidArgumentError(
         absl_ports::StrCat("Field 'property_name' '", property_name,
@@ -986,13 +1158,20 @@
 
 const SchemaUtil::SchemaDelta SchemaUtil::ComputeCompatibilityDelta(
     const SchemaProto& old_schema, const SchemaProto& new_schema,
-    const DependentMap& new_schema_dependent_map) {
+    const DependentMap& new_schema_dependent_map,
+    const FeatureFlags& feature_flags) {
   SchemaDelta schema_delta;
 
   TypeConfigMap old_type_config_map, new_type_config_map;
   BuildTypeConfigMap(old_schema, &old_type_config_map);
   BuildTypeConfigMap(new_schema, &new_type_config_map);
 
+  if (feature_flags.enable_scorable_properties()) {
+    FindScorablePropertyInconsistentTypes(
+        old_type_config_map, new_type_config_map, new_schema_dependent_map,
+        &schema_delta);
+  }
+
   // Iterate through and check each field of the old schema
   for (const auto& old_type_config : old_schema.types()) {
     auto new_schema_type_and_config =
diff --git a/icing/schema/schema-util.h b/icing/schema/schema-util.h
index 4f09915..27ad3e4 100644
--- a/icing/schema/schema-util.h
+++ b/icing/schema/schema-util.h
@@ -23,6 +23,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/proto/schema.pb.h"
 
 namespace icing {
@@ -95,6 +96,11 @@
     // field in the SchemaTypeConfigProto.
     std::unordered_set<std::string> schema_types_join_incompatible;
 
+    // Schema types that were changed in a way that was backwards compatible,
+    // but inconsistent with the old schema so that the scorable property cache
+    // needs to be re-generated.
+    std::unordered_set<std::string> schema_types_scorable_property_inconsistent;
+
     bool operator==(const SchemaDelta& other) const {
       return schema_types_deleted == other.schema_types_deleted &&
              schema_types_incompatible == other.schema_types_incompatible &&
@@ -104,7 +110,9 @@
              schema_types_index_incompatible ==
                  other.schema_types_index_incompatible &&
              schema_types_join_incompatible ==
-                 other.schema_types_join_incompatible;
+                 other.schema_types_join_incompatible &&
+             schema_types_scorable_property_inconsistent ==
+                 other.schema_types_scorable_property_inconsistent;
     }
   };
 
@@ -163,6 +171,14 @@
   //  15. For DOCUMENT data types, if
   //      DocumentIndexingConfig.indexable_nested_properties_list is non-empty,
   //      DocumentIndexingConfig.index_nested_properties must be false.
+  //  16. Validate the PropertyConfigProtos.scorable_type:
+  //        - It can only be set to ENABLED for the following data types:
+  //            a. Int64
+  //            b. Double
+  //            c. Boolean
+  //        - Documment type can't be explicitly set to DISABLED OR
+  //          ENABLED. It is implicitly considered scorable if any of its or its
+  //          dependency's property is scorable.
   //
   // Returns:
   //   On success, a dependent map from each types to their dependent types
@@ -170,7 +186,8 @@
   //   ALREADY_EXISTS for case 1 and 2
   //   INVALID_ARGUMENT for 3-15
   static libtextclassifier3::StatusOr<DependentMap> Validate(
-      const SchemaProto& schema, bool allow_circular_schema_definitions);
+      const SchemaProto& schema, const FeatureFlags& feature_flags,
+      bool allow_circular_schema_definitions);
 
   // Builds a transitive inheritance map.
   //
@@ -215,6 +232,8 @@
   //      definition is now incompatible.
   //   4. The derived join index would be incompatible. This is held in
   //      `SchemaDelta.join_incompatible`.
+  //   5. The scorable properties of two schema are inconsistent. This is held
+  //      in `SchemaDelta.schema_types_scorable_property_inconsistent`.
   //
   // For case 1, the two schemas would result in an incompatible index if:
   //   1.1. The new SchemaProto has a different set of indexed properties than
@@ -242,13 +261,28 @@
   //        different set of joinable properties than it did in the old
   //        SchemaProto.
   //
+  // For case 5, a schema type is considered to have inconsistent scorable
+  // properties if it is present in both the old and new schemas, and that:
+  //   5.1. The schema type contains different sets of scorable properties in
+  //        the old and new schemas. It could be that:
+  //          a. The type contains scorable properties in the new schema, but
+  //             not in the old schema.
+  //          b. The type contains scorable properties in the old schema, but
+  //             not in the new schema.
+  //          c. The type contains scorable properties in both the old and new
+  //             schemas, but the set of properties are different.
+  //   5.2. The type has dependency on the types that are considered to have
+  //        inconsistent scorable properties, based on the new schema's
+  //        dependent map.
+  //
   // A property is defined by the combination of the
   // SchemaTypeConfig.schema_type and the PropertyConfigProto.property_name.
   //
   // Returns a SchemaDelta that captures the aforementioned differences.
   static const SchemaDelta ComputeCompatibilityDelta(
       const SchemaProto& old_schema, const SchemaProto& new_schema,
-      const DependentMap& new_schema_dependent_map);
+      const DependentMap& new_schema_dependent_map,
+      const FeatureFlags& feature_flags);
 
   // Validates the 'property_name' field.
   //   1. Can't be an empty string
@@ -293,6 +327,15 @@
       PropertyConfigProto::Cardinality::Code cardinality,
       std::string_view schema_type, std::string_view property_name);
 
+  // Validates the scorable_type of the given |property_config_proto|.
+  //
+  // Returns:
+  //   INVALID_ARGUMENT if any scorable_type is found to be set incorrectly.
+  //   OK on success
+  static libtextclassifier3::Status ValidateScorableType(
+      std::string_view schema_type,
+      const PropertyConfigProto& property_config_proto);
+
   // Checks that the 'string_indexing_config' satisfies the following rules:
   //   1. Only STRING data types can be indexed
   //   2. An indexed property must have a valid tokenizer type
@@ -319,7 +362,8 @@
       const JoinableConfig& config,
       PropertyConfigProto::DataType::Code data_type,
       PropertyConfigProto::Cardinality::Code cardinality,
-      std::string_view schema_type, std::string_view property_name);
+      std::string_view schema_type, std::string_view property_name,
+      const FeatureFlags& feature_flags);
 
   // Checks that the 'document_indexing_config' satisfies the following rule:
   //    1. If indexable_nested_properties is non-empty, index_nested_properties
diff --git a/icing/schema/schema-util_test.cc b/icing/schema/schema-util_test.cc
index e77ffab..414525e 100644
--- a/icing/schema/schema-util_test.cc
+++ b/icing/schema/schema-util_test.cc
@@ -15,15 +15,20 @@
 #include "icing/schema/schema-util.h"
 
 #include <initializer_list>
+#include <memory>
 #include <string>
 #include <string_view>
 #include <unordered_set>
+#include <utility>
 
+#include "icing/text_classifier/lib3/utils/base/status.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "icing/feature-flags.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/schema-builder.h"
 #include "icing/testing/common-matchers.h"
+#include "icing/testing/test-feature-flags.h"
 
 namespace icing {
 namespace lib {
@@ -45,7 +50,14 @@
 constexpr char kMessageType[] = "Text";
 constexpr char kPersonType[] = "Person";
 
-class SchemaUtilTest : public ::testing::TestWithParam<bool> {};
+class SchemaUtilTest : public ::testing::TestWithParam<bool> {
+ protected:
+  void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+  }
+
+  std::unique_ptr<FeatureFlags> feature_flags_;
+};
 
 TEST_P(SchemaUtilTest, DependentGraphAlphabeticalOrder) {
   // Create a schema with the following dependent relation:
@@ -123,8 +135,9 @@
                            .AddType(type_e)
                            .AddType(type_f)
                            .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, testing::SizeIs(5));
   EXPECT_THAT(
       d_map["F"],
@@ -231,8 +244,9 @@
                            .AddType(type_b)
                            .AddType(type_a)
                            .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, testing::SizeIs(5));
   EXPECT_THAT(
       d_map["F"],
@@ -338,8 +352,9 @@
                            .AddType(type_b)
                            .AddType(type_d)
                            .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, testing::SizeIs(5));
   EXPECT_THAT(
       d_map["F"],
@@ -392,7 +407,7 @@
           .Build();
 
   SchemaProto schema = SchemaBuilder().AddType(type_a).AddType(type_b).Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("Invalid cycle")));
 }
@@ -428,8 +443,9 @@
 
   SchemaProto schema = SchemaBuilder().AddType(type_a).AddType(type_b).Build();
   // Assert Validate status is OK and check dependent map
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, SizeIs(1));
   EXPECT_THAT(d_map["B"],
               UnorderedElementsAre(
@@ -473,7 +489,7 @@
 
   SchemaProto schema =
       SchemaBuilder().AddType(type_a).AddType(type_b).AddType(type_c).Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs((libtextclassifier3::StatusCode::INVALID_ARGUMENT),
                        HasSubstr("Invalid cycle")));
 }
@@ -517,7 +533,7 @@
 
   SchemaProto schema =
       SchemaBuilder().AddType(type_a).AddType(type_b).AddType(type_c).Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::OK));
 }
 
@@ -563,8 +579,9 @@
   SchemaProto schema =
       SchemaBuilder().AddType(type_a).AddType(type_b).AddType(type_c).Build();
   // Assert Validate status is OK and check dependent map
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, SizeIs(3));
   EXPECT_THAT(
       d_map["A"],
@@ -636,7 +653,7 @@
                            .AddType(type_c)
                            .AddType(type_d)
                            .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("Invalid cycle")));
 }
@@ -704,8 +721,9 @@
                            .AddType(type_d)
                            .Build();
   // Assert Validate status is OK and check dependent map
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, SizeIs(3));
   EXPECT_THAT(d_map["B"],
               UnorderedElementsAre(
@@ -780,7 +798,7 @@
                            .AddType(type_c)
                            .AddType(type_b)
                            .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("Invalid cycle")));
 }
@@ -850,7 +868,7 @@
                            .AddType(type_b)
                            .AddType(type_a)
                            .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -891,8 +909,9 @@
 
   SchemaProto schema = SchemaBuilder().AddType(type_a).AddType(type_b).Build();
   // Assert Validate status is OK and check dependent map
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, SizeIs(2));
   EXPECT_THAT(
       d_map["A"],
@@ -938,7 +957,7 @@
           .Build();
 
   SchemaProto schema = SchemaBuilder().AddType(type_a).AddType(type_b).Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("Invalid cycle")));
 }
@@ -986,7 +1005,7 @@
 
   SchemaProto schema =
       SchemaBuilder().AddType(type_a).AddType(type_b).AddType(type_c).Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("Invalid cycle")));
 }
@@ -1038,8 +1057,9 @@
   SchemaProto schema =
       SchemaBuilder().AddType(type_a).AddType(type_b).AddType(type_c).Build();
   // Assert Validate status is OK and check dependent map
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, SizeIs(2));
   EXPECT_THAT(d_map["B"],
               UnorderedElementsAre(
@@ -1114,7 +1134,7 @@
                            .AddType(type_c)
                            .AddType(type_d)
                            .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("Invalid cycle")));
 }
@@ -1187,7 +1207,7 @@
                            .AddType(type_d)
                            .AddType(type_e)
                            .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("Invalid cycle")));
 }
@@ -1261,7 +1281,7 @@
                            .AddType(type_d)
                            .AddType(type_e)
                            .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("Invalid cycle")));
 }
@@ -1357,7 +1377,7 @@
                            .AddType(type_e)
                            .AddType(type_f)
                            .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("Invalid cycle")));
 }
@@ -1449,7 +1469,7 @@
                            .AddType(type_e)
                            .AddType(type_f)
                            .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("Invalid cycle")));
 }
@@ -1523,8 +1543,9 @@
 
   SchemaProto schema =
       SchemaBuilder().AddType(type_a).AddType(type_b).AddType(type_c).Build();
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, SizeIs(3));
   // Both A-B and A-C are inheritance relations.
   EXPECT_THAT(d_map["A"],
@@ -1617,7 +1638,7 @@
 
   SchemaProto schema =
       SchemaBuilder().AddType(type_a).AddType(type_b).AddType(type_c).Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("Invalid cycle")));
 }
@@ -1683,7 +1704,7 @@
 
   SchemaProto schema =
       SchemaBuilder().AddType(type_a).AddType(type_b).AddType(type_c).Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("inherits from itself")));
 }
@@ -1721,7 +1742,7 @@
 
   SchemaProto schema =
       SchemaBuilder().AddType(type_a).AddType(type_b).AddType(type_c).Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -1761,8 +1782,9 @@
 
   SchemaProto schema =
       SchemaBuilder().AddType(type_a).AddType(type_b).AddType(type_c).Build();
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, SizeIs(2));
   EXPECT_THAT(d_map["A"],
               UnorderedElementsAre(
@@ -1788,8 +1810,9 @@
       SchemaTypeConfigBuilder().SetType("B").AddParentType("A").Build();
 
   SchemaProto schema = SchemaBuilder().AddType(type_a).AddType(type_b).Build();
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, SizeIs(1));
   EXPECT_THAT(d_map["A"], UnorderedElementsAre(Pair("B", IsEmpty())));
 
@@ -1819,8 +1842,9 @@
 
   SchemaProto schema =
       SchemaBuilder().AddType(type_a).AddType(type_b).AddType(type_c).Build();
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, SizeIs(2));
   EXPECT_THAT(d_map["A"],
               UnorderedElementsAre(Pair("B", IsEmpty()), Pair("C", IsEmpty())));
@@ -1864,8 +1888,9 @@
                            .AddType(type_e)
                            .AddType(type_f)
                            .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, SizeIs(3));
   EXPECT_THAT(d_map["A"],
               UnorderedElementsAre(Pair("B", IsEmpty()), Pair("C", IsEmpty()),
@@ -1902,7 +1927,7 @@
 
   SchemaProto schema =
       SchemaBuilder().AddType(type_a).AddType(type_b).AddType(type_c).Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -1911,7 +1936,7 @@
       SchemaTypeConfigBuilder().SetType("A").AddParentType("A").Build();
 
   SchemaProto schema = SchemaBuilder().AddType(type_a).Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -1927,7 +1952,7 @@
 
   SchemaProto schema =
       SchemaBuilder().AddType(type_a).AddType(type_b).AddType(type_c).Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -1950,8 +1975,9 @@
 
   SchemaProto schema =
       SchemaBuilder().AddType(type_a).AddType(type_b).AddType(type_c).Build();
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, SizeIs(2));
   // Nested-type dependency and inheritance dependencies are not transitive.
   EXPECT_THAT(d_map["A"], UnorderedElementsAre(Pair("B", IsEmpty())));
@@ -2022,8 +2048,9 @@
                            .AddType(type_e)
                            .AddType(type_f)
                            .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, SizeIs(3));
   EXPECT_THAT(
       d_map["A"],
@@ -2065,13 +2092,13 @@
       SchemaTypeConfigBuilder().SetType("B").AddParentType("A").Build();
 
   SchemaProto schema = SchemaBuilder().AddType(type_a).AddType(type_b).Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
 TEST_P(SchemaUtilTest, EmptySchemaProtoIsValid) {
   SchemaProto schema;
-  ICING_ASSERT_OK(SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK(SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
 }
 
 TEST_P(SchemaUtilTest, Valid_Nested) {
@@ -2097,7 +2124,7 @@
                                         .SetCardinality(CARDINALITY_REQUIRED)))
           .Build();
 
-  ICING_ASSERT_OK(SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK(SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
 }
 
 TEST_P(SchemaUtilTest, ClearedPropertyConfigsIsValid) {
@@ -2106,13 +2133,13 @@
       SchemaBuilder()
           .AddType(SchemaTypeConfigBuilder().SetType(kEmailType))
           .Build();
-  ICING_ASSERT_OK(SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK(SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
 }
 
 TEST_P(SchemaUtilTest, ClearedSchemaTypeIsInvalid) {
   SchemaProto schema =
       SchemaBuilder().AddType(SchemaTypeConfigBuilder()).Build();
-  ASSERT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  ASSERT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -2120,7 +2147,7 @@
   SchemaProto schema =
       SchemaBuilder().AddType(SchemaTypeConfigBuilder().SetType("")).Build();
 
-  ASSERT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  ASSERT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -2130,7 +2157,7 @@
                                "abc123!@#$%^&*()_-+=[{]}|\\;:'\",<.>?你弽"))
                            .Build();
 
-  ICING_ASSERT_OK(SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK(SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
 }
 
 TEST_P(SchemaUtilTest, ClearedPropertyNameIsInvalid) {
@@ -2144,7 +2171,7 @@
                                         .SetCardinality(CARDINALITY_REQUIRED)))
           .Build();
   schema.mutable_types(0)->mutable_properties(0)->clear_property_name();
-  ASSERT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  ASSERT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -2159,7 +2186,7 @@
                                         .SetCardinality(CARDINALITY_REQUIRED)))
           .Build();
 
-  ASSERT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  ASSERT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -2174,7 +2201,7 @@
                                         .SetCardinality(CARDINALITY_REQUIRED)))
           .Build();
 
-  ASSERT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  ASSERT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -2189,7 +2216,7 @@
                                         .SetCardinality(CARDINALITY_REQUIRED)))
           .Build();
 
-  ICING_ASSERT_OK(SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK(SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
 }
 
 TEST_P(SchemaUtilTest, DuplicatePropertyNameIsInvalid) {
@@ -2206,7 +2233,7 @@
                                         .SetDataType(TYPE_STRING)
                                         .SetCardinality(CARDINALITY_REQUIRED)))
           .Build();
-  ASSERT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  ASSERT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::ALREADY_EXISTS));
 }
 
@@ -2221,7 +2248,7 @@
                                         .SetCardinality(CARDINALITY_REQUIRED)))
           .Build();
   schema.mutable_types(0)->mutable_properties(0)->clear_data_type();
-  ASSERT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  ASSERT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -2237,7 +2264,7 @@
                           .SetDataType(PropertyConfigProto::DataType::UNKNOWN)
                           .SetCardinality(CARDINALITY_REQUIRED)))
           .Build();
-  ASSERT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  ASSERT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -2252,7 +2279,7 @@
                                         .SetCardinality(CARDINALITY_REQUIRED)))
           .Build();
   schema.mutable_types(0)->mutable_properties(0)->clear_cardinality();
-  ASSERT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  ASSERT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -2266,7 +2293,7 @@
                                         .SetDataType(TYPE_STRING)
                                         .SetCardinality(CARDINALITY_UNKNOWN)))
           .Build();
-  ASSERT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  ASSERT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -2280,7 +2307,7 @@
                                         .SetDataType(TYPE_DOCUMENT)
                                         .SetCardinality(CARDINALITY_REPEATED)))
           .Build();
-  ASSERT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  ASSERT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -2297,7 +2324,7 @@
                                         .SetCardinality(CARDINALITY_REQUIRED)))
           .Build();
 
-  ASSERT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  ASSERT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -2314,7 +2341,7 @@
                                         .SetCardinality(CARDINALITY_REQUIRED)))
           .Build();
 
-  ASSERT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  ASSERT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("Undefined 'schema_type'")));
 }
@@ -2351,7 +2378,8 @@
   schema_delta.schema_types_changed_fully_compatible.insert(kEmailType);
   SchemaUtil::DependentMap no_dependents_map;
   EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
-                  old_schema, new_schema_with_optional, no_dependents_map),
+                  old_schema, new_schema_with_optional, no_dependents_map,
+                  *feature_flags_),
               Eq(schema_delta));
 }
 
@@ -2387,7 +2415,8 @@
   schema_delta.schema_types_incompatible.emplace(kEmailType);
   SchemaUtil::DependentMap no_dependents_map;
   EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
-                  old_schema, new_schema_with_required, no_dependents_map),
+                  old_schema, new_schema_with_required, no_dependents_map,
+                  *feature_flags_),
               Eq(schema_delta));
 }
 
@@ -2422,8 +2451,8 @@
   SchemaUtil::SchemaDelta schema_delta;
   schema_delta.schema_types_incompatible.emplace(kEmailType);
   SchemaUtil::DependentMap no_dependents_map;
-  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                                    no_dependents_map),
+  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
+                  old_schema, new_schema, no_dependents_map, *feature_flags_),
               Eq(schema_delta));
 }
 
@@ -2456,7 +2485,8 @@
   SchemaUtil::DependentMap no_dependents_map;
   EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
                   /*old_schema=*/less_restrictive_schema,
-                  /*new_schema=*/more_restrictive_schema, no_dependents_map),
+                  /*new_schema=*/more_restrictive_schema, no_dependents_map,
+                  *feature_flags_),
               Eq(incompatible_schema_delta));
 
   // We can have the new schema be less restrictive, OPTIONAL->REPEATED;
@@ -2465,7 +2495,8 @@
       kEmailType);
   EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
                   /*old_schema=*/more_restrictive_schema,
-                  /*new_schema=*/less_restrictive_schema, no_dependents_map),
+                  /*new_schema=*/less_restrictive_schema, no_dependents_map,
+                  *feature_flags_),
               Eq(compatible_schema_delta));
 }
 
@@ -2495,8 +2526,8 @@
   SchemaUtil::SchemaDelta schema_delta;
   schema_delta.schema_types_incompatible.emplace(kEmailType);
   SchemaUtil::DependentMap no_dependents_map;
-  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                                    no_dependents_map),
+  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
+                  old_schema, new_schema, no_dependents_map, *feature_flags_),
               Eq(schema_delta));
 }
 
@@ -2557,7 +2588,7 @@
   SchemaUtil::DependentMap dependents_map = {
       {kMessageType, {{kEmailType, {}}}}};
   SchemaUtil::SchemaDelta actual = SchemaUtil::ComputeCompatibilityDelta(
-      old_schema, new_schema, dependents_map);
+      old_schema, new_schema, dependents_map, *feature_flags_);
   EXPECT_THAT(actual, Eq(schema_delta));
   EXPECT_THAT(actual.schema_types_incompatible,
               testing::ElementsAre(kEmailType));
@@ -2594,7 +2625,7 @@
           .Build();
 
   SchemaUtil::SchemaDelta delta = SchemaUtil::ComputeCompatibilityDelta(
-      old_schema, new_schema, /*new_schema_dependent_map=*/{});
+      old_schema, new_schema, /*new_schema_dependent_map=*/{}, *feature_flags_);
   EXPECT_THAT(delta.schema_types_incompatible,
               testing::ElementsAre(kEmailType));
   EXPECT_THAT(delta.schema_types_index_incompatible, testing::IsEmpty());
@@ -2629,7 +2660,7 @@
           .Build();
 
   SchemaUtil::SchemaDelta delta = SchemaUtil::ComputeCompatibilityDelta(
-      old_schema, new_schema, /*new_schema_dependent_map=*/{});
+      old_schema, new_schema, /*new_schema_dependent_map=*/{}, *feature_flags_);
   EXPECT_THAT(delta.schema_types_incompatible, testing::IsEmpty());
   EXPECT_THAT(delta.schema_types_index_incompatible,
               testing::ElementsAre(kEmailType));
@@ -2664,7 +2695,7 @@
           .Build();
 
   SchemaUtil::SchemaDelta delta = SchemaUtil::ComputeCompatibilityDelta(
-      old_schema, new_schema, /*new_schema_dependent_map=*/{});
+      old_schema, new_schema, /*new_schema_dependent_map=*/{}, *feature_flags_);
   EXPECT_THAT(delta.schema_types_incompatible, testing::IsEmpty());
   EXPECT_THAT(delta.schema_types_index_incompatible, testing::IsEmpty());
   EXPECT_THAT(delta.schema_types_deleted, testing::IsEmpty());
@@ -2704,13 +2735,13 @@
   SchemaUtil::DependentMap no_dependents_map;
   EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
                   schema_with_unindexed_property, schema_with_indexed_property,
-                  no_dependents_map),
+                  no_dependents_map, *feature_flags_),
               Eq(schema_delta));
 
   // New schema lost an indexed string property.
   EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
                   schema_with_indexed_property, schema_with_unindexed_property,
-                  no_dependents_map),
+                  no_dependents_map, *feature_flags_),
               Eq(schema_delta));
 }
 
@@ -2747,8 +2778,8 @@
   SchemaUtil::SchemaDelta schema_delta;
   schema_delta.schema_types_index_incompatible.insert(kPersonType);
   SchemaUtil::DependentMap no_dependents_map;
-  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                                    no_dependents_map),
+  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
+                  old_schema, new_schema, no_dependents_map, *feature_flags_),
               Eq(schema_delta));
 }
 
@@ -2784,8 +2815,8 @@
           .Build();
 
   SchemaUtil::DependentMap no_dependents_map;
-  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                                    no_dependents_map)
+  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
+                  old_schema, new_schema, no_dependents_map, *feature_flags_)
                   .schema_types_index_incompatible,
               IsEmpty());
 }
@@ -2820,13 +2851,13 @@
   SchemaUtil::DependentMap no_dependents_map;
   EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
                   schema_with_unindexed_property, schema_with_indexed_property,
-                  no_dependents_map),
+                  no_dependents_map, *feature_flags_),
               Eq(schema_delta));
 
   // New schema lost an indexed integer property.
   EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
                   schema_with_indexed_property, schema_with_unindexed_property,
-                  no_dependents_map),
+                  no_dependents_map, *feature_flags_),
               Eq(schema_delta));
 }
 
@@ -2860,8 +2891,8 @@
   SchemaUtil::SchemaDelta schema_delta;
   schema_delta.schema_types_index_incompatible.insert(kPersonType);
   SchemaUtil::DependentMap no_dependents_map;
-  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                                    no_dependents_map),
+  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
+                  old_schema, new_schema, no_dependents_map, *feature_flags_),
               Eq(schema_delta));
 }
 
@@ -2894,8 +2925,8 @@
           .Build();
 
   SchemaUtil::DependentMap no_dependents_map;
-  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                                    no_dependents_map)
+  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
+                  old_schema, new_schema, no_dependents_map, *feature_flags_)
                   .schema_types_index_incompatible,
               IsEmpty());
 }
@@ -2930,16 +2961,58 @@
   SchemaUtil::DependentMap no_dependents_map;
   EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
                   schema_with_unindexed_property, schema_with_indexed_property,
-                  no_dependents_map),
+                  no_dependents_map, *feature_flags_),
               Eq(schema_delta));
 
   // New schema lost an indexed vector property.
   EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
                   schema_with_indexed_property, schema_with_unindexed_property,
-                  no_dependents_map),
+                  no_dependents_map, *feature_flags_),
               Eq(schema_delta));
 }
 
+TEST_P(SchemaUtilTest, ChangingQuantizationTypeMakesIndexIncompatible) {
+  SchemaProto schema_with_unquantized_property =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType(kPersonType)
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("Property")
+                                        .SetDataTypeVector(
+                                            EMBEDDING_INDEXING_LINEAR_SEARCH,
+                                            QUANTIZATION_TYPE_NONE)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  SchemaProto schema_with_quantized_property =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType(kPersonType)
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("Property")
+                                        .SetDataTypeVector(
+                                            EMBEDDING_INDEXING_LINEAR_SEARCH,
+                                            QUANTIZATION_TYPE_QUANTIZE_8_BIT)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  SchemaUtil::SchemaDelta schema_delta;
+  schema_delta.schema_types_index_incompatible.insert(kPersonType);
+
+  SchemaUtil::DependentMap no_dependents_map;
+  EXPECT_THAT(
+      SchemaUtil::ComputeCompatibilityDelta(schema_with_quantized_property,
+                                            schema_with_unquantized_property,
+                                            no_dependents_map, *feature_flags_),
+      Eq(schema_delta));
+
+  EXPECT_THAT(
+      SchemaUtil::ComputeCompatibilityDelta(schema_with_unquantized_property,
+                                            schema_with_quantized_property,
+                                            no_dependents_map, *feature_flags_),
+      Eq(schema_delta));
+}
+
 TEST_P(SchemaUtilTest, AddingNewIndexedVectorPropertyMakesIndexIncompatible) {
   // Configure old schema
   SchemaProto old_schema =
@@ -2971,8 +3044,8 @@
   SchemaUtil::SchemaDelta schema_delta;
   schema_delta.schema_types_index_incompatible.insert(kPersonType);
   SchemaUtil::DependentMap no_dependents_map;
-  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                                    no_dependents_map),
+  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
+                  old_schema, new_schema, no_dependents_map, *feature_flags_),
               Eq(schema_delta));
 }
 
@@ -3006,8 +3079,8 @@
           .Build();
 
   SchemaUtil::DependentMap no_dependents_map;
-  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                                    no_dependents_map)
+  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
+                  old_schema, new_schema, no_dependents_map, *feature_flags_)
                   .schema_types_index_incompatible,
               IsEmpty());
 }
@@ -3060,7 +3133,7 @@
   SchemaUtil::DependentMap dependents_map = {{kEmailType, {{kPersonType, {}}}}};
   SchemaUtil::SchemaDelta result_schema_delta =
       SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                            dependents_map);
+                                            dependents_map, *feature_flags_);
   EXPECT_THAT(result_schema_delta, Eq(schema_delta));
 }
 
@@ -3117,7 +3190,7 @@
   SchemaUtil::DependentMap dependents_map = {{kEmailType, {{kPersonType, {}}}}};
   SchemaUtil::SchemaDelta result_schema_delta =
       SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                            dependents_map);
+                                            dependents_map, *feature_flags_);
   EXPECT_THAT(result_schema_delta, Eq(schema_delta));
 }
 
@@ -3169,7 +3242,7 @@
   SchemaUtil::DependentMap dependents_map = {{kEmailType, {{kPersonType, {}}}}};
   SchemaUtil::SchemaDelta result_schema_delta =
       SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                            dependents_map);
+                                            dependents_map, *feature_flags_);
   EXPECT_THAT(result_schema_delta, Eq(schema_delta));
 }
 
@@ -3233,7 +3306,7 @@
   SchemaUtil::DependentMap dependents_map = {{kEmailType, {{kPersonType, {}}}}};
   SchemaUtil::SchemaDelta result_schema_delta =
       SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                            dependents_map);
+                                            dependents_map, *feature_flags_);
   EXPECT_THAT(result_schema_delta, Eq(schema_delta));
 }
 
@@ -3296,7 +3369,7 @@
   SchemaUtil::DependentMap dependents_map = {{kEmailType, {{kPersonType, {}}}}};
   SchemaUtil::SchemaDelta result_schema_delta =
       SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                            dependents_map);
+                                            dependents_map, *feature_flags_);
   EXPECT_THAT(result_schema_delta, Eq(schema_delta));
 }
 
@@ -3368,7 +3441,7 @@
   SchemaUtil::DependentMap dependents_map = {{kEmailType, {{kPersonType, {}}}}};
   SchemaUtil::SchemaDelta result_schema_delta =
       SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                            dependents_map);
+                                            dependents_map, *feature_flags_);
   EXPECT_THAT(result_schema_delta, Eq(schema_delta));
 }
 
@@ -3439,7 +3512,7 @@
   SchemaUtil::DependentMap dependents_map = {{kEmailType, {{kPersonType, {}}}}};
   SchemaUtil::SchemaDelta result_schema_delta =
       SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                            dependents_map);
+                                            dependents_map, *feature_flags_);
   EXPECT_THAT(result_schema_delta, Eq(schema_delta));
 }
 
@@ -3473,16 +3546,18 @@
 
   // New schema gained a new joinable property.
   SchemaUtil::DependentMap no_dependents_map;
-  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
-                  schema_with_non_joinable_property,
-                  schema_with_joinable_property, no_dependents_map),
-              Eq(expected_schema_delta));
+  EXPECT_THAT(
+      SchemaUtil::ComputeCompatibilityDelta(schema_with_non_joinable_property,
+                                            schema_with_joinable_property,
+                                            no_dependents_map, *feature_flags_),
+      Eq(expected_schema_delta));
 
   // New schema lost a joinable property.
-  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
-                  schema_with_joinable_property,
-                  schema_with_non_joinable_property, no_dependents_map),
-              Eq(expected_schema_delta));
+  EXPECT_THAT(
+      SchemaUtil::ComputeCompatibilityDelta(schema_with_joinable_property,
+                                            schema_with_non_joinable_property,
+                                            no_dependents_map, *feature_flags_),
+      Eq(expected_schema_delta));
 }
 
 TEST_P(SchemaUtilTest, AddingNewJoinablePropertyMakesJoinIncompatible) {
@@ -3518,8 +3593,8 @@
   SchemaUtil::SchemaDelta expected_schema_delta;
   expected_schema_delta.schema_types_join_incompatible.insert(kPersonType);
   SchemaUtil::DependentMap no_dependents_map;
-  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                                    no_dependents_map),
+  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
+                  old_schema, new_schema, no_dependents_map, *feature_flags_),
               Eq(expected_schema_delta));
 }
 
@@ -3554,12 +3629,46 @@
           .Build();
 
   SchemaUtil::DependentMap no_dependents_map;
-  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                                    no_dependents_map)
+  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
+                  old_schema, new_schema, no_dependents_map, *feature_flags_)
                   .schema_types_join_incompatible,
               IsEmpty());
 }
 
+TEST_P(SchemaUtilTest, ChangingJoinablePropertiesPropagateDeleteIsCompatible) {
+  // Configure old schema
+  SchemaProto old_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("MyType").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("Foo")
+                  .SetDataType(TYPE_STRING)
+                  .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                               DELETE_PROPAGATION_TYPE_NONE)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+
+  // Configure new schema with delete propagation type PROPAGATE_FROM
+  SchemaProto new_schema_with_optional =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("MyType").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("Foo")
+                  .SetDataType(TYPE_STRING)
+                  .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                               DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+
+  SchemaUtil::SchemaDelta schema_delta;
+  schema_delta.schema_types_changed_fully_compatible.insert("MyType");
+  SchemaUtil::DependentMap no_dependents_map;
+  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
+                  old_schema, new_schema_with_optional, no_dependents_map,
+                  *feature_flags_),
+              Eq(schema_delta));
+}
+
 TEST_P(SchemaUtilTest, AddingTypeIsCompatible) {
   // Can add a new type, existing data isn't incompatible, since none of them
   // are of this new schema type
@@ -3595,8 +3704,8 @@
   SchemaUtil::SchemaDelta schema_delta;
   schema_delta.schema_types_new.insert(kEmailType);
   SchemaUtil::DependentMap no_dependents_map;
-  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                                    no_dependents_map),
+  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
+                  old_schema, new_schema, no_dependents_map, *feature_flags_),
               Eq(schema_delta));
 }
 
@@ -3636,8 +3745,8 @@
   SchemaUtil::SchemaDelta schema_delta;
   schema_delta.schema_types_deleted.emplace(kPersonType);
   SchemaUtil::DependentMap no_dependents_map;
-  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
-                                                    no_dependents_map),
+  EXPECT_THAT(SchemaUtil::ComputeCompatibilityDelta(
+                  old_schema, new_schema, no_dependents_map, *feature_flags_),
               Eq(schema_delta));
 }
 
@@ -3675,7 +3784,7 @@
   schema_delta.schema_types_index_incompatible.emplace(kEmailType);
   SchemaUtil::DependentMap no_dependents_map;
   SchemaUtil::SchemaDelta actual = SchemaUtil::ComputeCompatibilityDelta(
-      old_schema, new_schema, no_dependents_map);
+      old_schema, new_schema, no_dependents_map, *feature_flags_);
   EXPECT_THAT(actual, Eq(schema_delta));
 }
 
@@ -3723,14 +3832,16 @@
   schema_delta.schema_types_index_incompatible.emplace(kPersonType);
   SchemaUtil::DependentMap dependents_map = {{kEmailType, {{kPersonType, {}}}}};
   SchemaUtil::SchemaDelta actual = SchemaUtil::ComputeCompatibilityDelta(
-      no_nested_index_schema, nested_index_schema, dependents_map);
+      no_nested_index_schema, nested_index_schema, dependents_map,
+      *feature_flags_);
   EXPECT_THAT(actual, Eq(schema_delta));
 
   // Going from index_nested_properties=true to index_nested_properties=false
   // should also make kPersonType index_incompatible. kEmailType should be
   // unaffected.
   actual = SchemaUtil::ComputeCompatibilityDelta(
-      nested_index_schema, no_nested_index_schema, dependents_map);
+      nested_index_schema, no_nested_index_schema, dependents_map,
+      *feature_flags_);
   EXPECT_THAT(actual, Eq(schema_delta));
 }
 
@@ -3785,14 +3896,14 @@
   SchemaUtil::SchemaDelta schema_delta;
   schema_delta.schema_types_index_incompatible.emplace(kPersonType);
   SchemaUtil::DependentMap dependents_map = {{kEmailType, {{kPersonType, {}}}}};
-  SchemaUtil::SchemaDelta actual =
-      SchemaUtil::ComputeCompatibilityDelta(schema_1, schema_2, dependents_map);
+  SchemaUtil::SchemaDelta actual = SchemaUtil::ComputeCompatibilityDelta(
+      schema_1, schema_2, dependents_map, *feature_flags_);
   EXPECT_THAT(actual, Eq(schema_delta));
 
   // Adding some indexable_nested_properties should also make kPersonType
   // index_incompatible. kEmailType should be unaffected.
-  actual =
-      SchemaUtil::ComputeCompatibilityDelta(schema_2, schema_1, dependents_map);
+  actual = SchemaUtil::ComputeCompatibilityDelta(
+      schema_2, schema_1, dependents_map, *feature_flags_);
   EXPECT_THAT(actual, Eq(schema_delta));
 }
 
@@ -3848,8 +3959,8 @@
   SchemaUtil::SchemaDelta schema_delta;
   schema_delta.schema_types_index_incompatible.emplace(kPersonType);
   SchemaUtil::DependentMap dependents_map = {{kEmailType, {{kPersonType, {}}}}};
-  SchemaUtil::SchemaDelta actual =
-      SchemaUtil::ComputeCompatibilityDelta(schema_1, schema_2, dependents_map);
+  SchemaUtil::SchemaDelta actual = SchemaUtil::ComputeCompatibilityDelta(
+      schema_1, schema_2, dependents_map, *feature_flags_);
   EXPECT_THAT(actual, Eq(schema_delta));
 }
 
@@ -3904,8 +4015,8 @@
   SchemaUtil::SchemaDelta schema_delta;
   schema_delta.schema_types_index_incompatible.emplace(kPersonType);
   SchemaUtil::DependentMap dependents_map = {{kEmailType, {{kPersonType, {}}}}};
-  SchemaUtil::SchemaDelta actual =
-      SchemaUtil::ComputeCompatibilityDelta(schema_1, schema_2, dependents_map);
+  SchemaUtil::SchemaDelta actual = SchemaUtil::ComputeCompatibilityDelta(
+      schema_1, schema_2, dependents_map, *feature_flags_);
   EXPECT_THAT(actual, Eq(schema_delta));
 }
 
@@ -3961,12 +4072,305 @@
   // no effect on schema compatibility.
   SchemaUtil::SchemaDelta schema_delta;
   SchemaUtil::DependentMap dependents_map = {{kEmailType, {{kPersonType, {}}}}};
-  SchemaUtil::SchemaDelta actual =
-      SchemaUtil::ComputeCompatibilityDelta(schema_1, schema_2, dependents_map);
+  SchemaUtil::SchemaDelta actual = SchemaUtil::ComputeCompatibilityDelta(
+      schema_1, schema_2, dependents_map, *feature_flags_);
   EXPECT_THAT(actual, Eq(schema_delta));
   EXPECT_THAT(actual.schema_types_index_incompatible, IsEmpty());
 }
 
+TEST_P(SchemaUtilTest, SchemasWithConsistentScorableProperties) {
+  SchemaProto old_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("title")
+                                        .SetDataType(TYPE_STRING)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("score")
+                                        .SetDataType(TYPE_DOUBLE)
+                                        .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+  SchemaProto new_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("email").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("score")
+                  .SetDataType(TYPE_DOUBLE)
+                  .SetScorableType(SCORABLE_TYPE_ENABLED)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap new_schema_dependent_map,
+      SchemaUtil::Validate(new_schema, *feature_flags_, GetParam()));
+
+  SchemaUtil::SchemaDelta schema_delta = SchemaUtil::ComputeCompatibilityDelta(
+      old_schema, new_schema, new_schema_dependent_map, *feature_flags_);
+  EXPECT_THAT(schema_delta.schema_types_scorable_property_inconsistent,
+              UnorderedElementsAre());
+}
+
+TEST_P(SchemaUtilTest,
+       SchemaDeltaScorablePropertyInconsistent_SchemaTypeRemovedInNewSchema) {
+  SchemaProto old_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("email").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("title")
+                  .SetDataType(TYPE_STRING)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .AddType(SchemaTypeConfigBuilder().SetType("person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("frequencyScore")
+                  .SetDataType(TYPE_DOUBLE)
+                  .SetScorableType(SCORABLE_TYPE_ENABLED)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+  // New schema removes "person" type.
+  SchemaProto new_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("email").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("title")
+                  .SetDataType(TYPE_STRING)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap new_schema_dependent_map,
+      SchemaUtil::Validate(new_schema, *feature_flags_, GetParam()));
+
+  SchemaUtil::SchemaDelta schema_delta = SchemaUtil::ComputeCompatibilityDelta(
+      old_schema, new_schema, new_schema_dependent_map, *feature_flags_);
+  EXPECT_THAT(schema_delta.schema_types_scorable_property_inconsistent,
+              UnorderedElementsAre());
+}
+
+TEST_P(SchemaUtilTest,
+       SchemaDeltaScorablePropertyInconsistent_SchemaTypeAddedInNewSchema) {
+  SchemaProto old_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("email").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("title")
+                  .SetDataType(TYPE_STRING)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .AddType(SchemaTypeConfigBuilder().SetType("message").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("frequencyScore")
+                  .SetDataType(TYPE_DOUBLE)
+                  .SetScorableType(SCORABLE_TYPE_ENABLED)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+  // New schema adds "person" type.
+  SchemaProto new_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("email").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("title")
+                  .SetDataType(TYPE_STRING)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .AddType(SchemaTypeConfigBuilder().SetType("message").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("frequencyScore")
+                  .SetDataType(TYPE_DOUBLE)
+                  .SetScorableType(SCORABLE_TYPE_ENABLED)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(SchemaTypeConfigBuilder().SetType("person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("frequencyScore")
+                  .SetDataType(TYPE_DOUBLE)
+                  .SetScorableType(SCORABLE_TYPE_ENABLED)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap new_schema_dependent_map,
+      SchemaUtil::Validate(new_schema, *feature_flags_, GetParam()));
+
+  SchemaUtil::SchemaDelta schema_delta = SchemaUtil::ComputeCompatibilityDelta(
+      old_schema, new_schema, new_schema_dependent_map, *feature_flags_);
+  EXPECT_THAT(schema_delta.schema_types_scorable_property_inconsistent,
+              UnorderedElementsAre());
+}
+
+TEST_P(SchemaUtilTest,
+       SchemaDeltaScorablePropertyInconsistent_AddScorableProperties) {
+  SchemaProto old_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("email").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("title")
+                  .SetDataType(TYPE_STRING)
+                  .SetCardinality(CARDINALITY_REQUIRED)))
+          .Build();
+  // New schema adds "score" as scorable_type ENABLED from type "email".
+  SchemaProto new_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("title")
+                                        .SetDataType(TYPE_STRING)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("score")
+                                        .SetDataType(TYPE_DOUBLE)
+                                        .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap new_schema_dependent_map,
+      SchemaUtil::Validate(new_schema, *feature_flags_, GetParam()));
+
+  SchemaUtil::SchemaDelta schema_delta = SchemaUtil::ComputeCompatibilityDelta(
+      old_schema, new_schema, new_schema_dependent_map, *feature_flags_);
+  EXPECT_THAT(schema_delta.schema_types_scorable_property_inconsistent,
+              UnorderedElementsAre("email"));
+}
+
+TEST_P(SchemaUtilTest,
+       SchemaDeltaScorablePropertyInconsistent_RemovesAllScorableProperties) {
+  SchemaProto old_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("title")
+                                        .SetDataType(TYPE_STRING)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("score")
+                                        .SetDataType(TYPE_DOUBLE)
+                                        .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+  // New schema marks "score" as scorable_type DISABLED from type "email".
+  SchemaProto new_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("title")
+                                        .SetDataType(TYPE_STRING)
+                                        .SetCardinality(CARDINALITY_REQUIRED))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("score")
+                                        .SetDataType(TYPE_DOUBLE)
+                                        .SetScorableType(SCORABLE_TYPE_DISABLED)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap new_schema_dependent_map,
+      SchemaUtil::Validate(new_schema, *feature_flags_, GetParam()));
+
+  SchemaUtil::SchemaDelta schema_delta = SchemaUtil::ComputeCompatibilityDelta(
+      old_schema, new_schema, new_schema_dependent_map, *feature_flags_);
+  EXPECT_THAT(schema_delta.schema_types_scorable_property_inconsistent,
+              UnorderedElementsAre("email"));
+}
+
+TEST_P(SchemaUtilTest,
+       SchemaDeltaScorablePropertyInconsistent_DifferentScorablePropertyNames) {
+  SchemaProto old_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("score1")
+                                        .SetDataType(TYPE_DOUBLE)
+                                        .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("score2")
+                                        .SetDataType(TYPE_DOUBLE)
+                                        .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+  SchemaProto new_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("email")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("score2")
+                                        .SetDataType(TYPE_DOUBLE)
+                                        .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                        .SetCardinality(CARDINALITY_OPTIONAL))
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("score3")
+                                        .SetDataType(TYPE_DOUBLE)
+                                        .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap new_schema_dependent_map,
+      SchemaUtil::Validate(new_schema, *feature_flags_, GetParam()));
+
+  SchemaUtil::SchemaDelta schema_delta = SchemaUtil::ComputeCompatibilityDelta(
+      old_schema, new_schema, new_schema_dependent_map, *feature_flags_);
+  EXPECT_THAT(schema_delta.schema_types_scorable_property_inconsistent,
+              UnorderedElementsAre("email"));
+}
+
+TEST_P(SchemaUtilTest,
+       SchemaDeltaScorablePropertyInconsistent_ReportParentTypes) {
+  SchemaProto old_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("scoreA")
+                  .SetDataType(TYPE_DOUBLE)
+                  .SetScorableType(SCORABLE_TYPE_ENABLED)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+
+  // The new schema's type 'person' has inconsistent scorable property with
+  // the old schema.
+  //
+  // The dependence map of the new schema:
+  //   personalContext -> gmail -> person
+  //   message -> person
+  SchemaProto new_schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("scoreB")
+                  .SetDataType(TYPE_DOUBLE)
+                  .SetScorableType(SCORABLE_TYPE_ENABLED)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(SchemaTypeConfigBuilder().SetType("message").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("sender")
+                  .SetDataTypeDocument("person",
+                                       /*index_nested_properties=*/true)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(SchemaTypeConfigBuilder().SetType("gmail").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("sender")
+                  .SetDataTypeDocument("person",
+                                       /*index_nested_properties=*/true)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(SchemaTypeConfigBuilder()
+                       .SetType("personalContext")
+                       .AddProperty(PropertyConfigBuilder()
+                                        .SetName("mailData")
+                                        .SetDataTypeDocument(
+                                            "gmail",
+                                            /*index_nested_properties=*/true)
+                                        .SetCardinality(CARDINALITY_OPTIONAL)))
+          .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap new_schema_dependent_map,
+      SchemaUtil::Validate(new_schema, *feature_flags_, GetParam()));
+
+  SchemaUtil::SchemaDelta schema_delta = SchemaUtil::ComputeCompatibilityDelta(
+      old_schema, new_schema, new_schema_dependent_map, *feature_flags_);
+  EXPECT_THAT(
+      schema_delta.schema_types_scorable_property_inconsistent,
+      UnorderedElementsAre("person", "gmail", "personalContext", "message"));
+}
+
 TEST_P(SchemaUtilTest, ValidateStringIndexingConfigShouldHaveTermMatchType) {
   SchemaProto schema =
       SchemaBuilder()
@@ -3978,7 +4382,7 @@
           .Build();
 
   // Error if we don't set a term match type
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
   // Passes once we set a term match type
@@ -3989,7 +4393,8 @@
                        .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
                        .SetCardinality(CARDINALITY_REQUIRED)))
                .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()), IsOk());
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
 }
 
 TEST_P(SchemaUtilTest, ValidateStringIndexingConfigShouldHaveTokenizer) {
@@ -4003,7 +4408,7 @@
           .Build();
 
   // Error if we don't set a tokenizer type
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
   // Passes once we set a tokenizer type
@@ -4014,7 +4419,8 @@
                        .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
                        .SetCardinality(CARDINALITY_REQUIRED)))
                .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()), IsOk());
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
 }
 
 TEST_P(SchemaUtilTest,
@@ -4026,12 +4432,12 @@
                   .SetName("Foo")
                   .SetDataType(TYPE_INT64)
                   .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                               /*propagate_delete=*/false)
+                               DELETE_PROPAGATION_TYPE_NONE)
                   .SetCardinality(CARDINALITY_REQUIRED)))
           .Build();
 
   // Error if data type is not STRING for qualified id joinable value type.
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
   // Passes once we set STRING as the data type.
@@ -4041,14 +4447,20 @@
                        .SetName("Foo")
                        .SetDataType(TYPE_STRING)
                        .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                                    /*propagate_delete=*/false)
+                                    DELETE_PROPAGATION_TYPE_NONE)
                        .SetCardinality(CARDINALITY_REQUIRED)))
                .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()), IsOk());
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
 }
 
 TEST_P(SchemaUtilTest,
        ValidateJoinablePropertyShouldNotHaveRepeatedCardinality) {
+  // We need to explicitly override enable_repeated_field_joins to false.
+  feature_flags_ =
+      std::make_unique<FeatureFlags>(/*enable_scorable_properties=*/true,
+                                     /*enable_embedding_quantization=*/true,
+                                     /*enable_repeated_field_joins=*/false);
   SchemaProto schema =
       SchemaBuilder()
           .AddType(SchemaTypeConfigBuilder().SetType("MyType").AddProperty(
@@ -4056,12 +4468,12 @@
                   .SetName("Foo")
                   .SetDataType(TYPE_STRING)
                   .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                               /*propagate_delete=*/false)
+                               DELETE_PROPAGATION_TYPE_NONE)
                   .SetCardinality(CARDINALITY_REPEATED)))
           .Build();
 
   // Error if using REPEATED cardinality for joinable property.
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
   // Passes once we use OPTIONAL cardinality with joinable property.
@@ -4071,10 +4483,11 @@
                        .SetName("Foo")
                        .SetDataType(TYPE_STRING)
                        .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                                    /*propagate_delete=*/false)
+                                    DELETE_PROPAGATION_TYPE_NONE)
                        .SetCardinality(CARDINALITY_OPTIONAL)))
                .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()), IsOk());
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
 
   // Passes once we use REQUIRED cardinality with joinable property.
   schema = SchemaBuilder()
@@ -4083,10 +4496,11 @@
                        .SetName("Foo")
                        .SetDataType(TYPE_STRING)
                        .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                                    /*propagate_delete=*/false)
+                                    DELETE_PROPAGATION_TYPE_NONE)
                        .SetCardinality(CARDINALITY_REQUIRED)))
                .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()), IsOk());
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
 
   // Passes once we use REPEATED cardinality with non-joinable property.
   schema = SchemaBuilder()
@@ -4095,10 +4509,34 @@
                        .SetName("Foo")
                        .SetDataType(TYPE_STRING)
                        .SetJoinable(JOINABLE_VALUE_TYPE_NONE,
-                                    /*propagate_delete=*/false)
+                                    DELETE_PROPAGATION_TYPE_NONE)
                        .SetCardinality(CARDINALITY_REPEATED)))
                .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()), IsOk());
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
+}
+
+TEST_P(SchemaUtilTest, ValidateJoinablePropertyCanHaveRepeatedCardinality) {
+  // We need to explicitly override enable_repeated_field_joins to true.
+  feature_flags_ =
+      std::make_unique<FeatureFlags>(/*enable_scorable_properties=*/true,
+                                     /*enable_embedding_quantization=*/true,
+                                     /*enable_repeated_field_joins=*/true);
+
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("MyType").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("Foo")
+                  .SetDataType(TYPE_STRING)
+                  .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                               DELETE_PROPAGATION_TYPE_NONE)
+                  .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+
+  // Error if using REPEATED cardinality for joinable property.
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
 }
 
 TEST_P(SchemaUtilTest,
@@ -4110,13 +4548,13 @@
                   .SetName("Foo")
                   .SetDataType(TYPE_STRING)
                   .SetJoinable(JOINABLE_VALUE_TYPE_NONE,
-                               /*propagate_delete=*/true)
+                               DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
                   .SetCardinality(CARDINALITY_REQUIRED)))
           .Build();
 
   // Error if enabling delete propagation with non qualified id joinable value
   // type.
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
   // Passes once we set qualified id joinable value type with delete propagation
@@ -4127,10 +4565,11 @@
                        .SetName("Foo")
                        .SetDataType(TYPE_STRING)
                        .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                                    /*propagate_delete=*/true)
+                                    DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
                        .SetCardinality(CARDINALITY_REQUIRED)))
                .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()), IsOk());
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
 
   // Passes once we disable delete propagation.
   schema = SchemaBuilder()
@@ -4139,10 +4578,11 @@
                        .SetName("Foo")
                        .SetDataType(TYPE_STRING)
                        .SetJoinable(JOINABLE_VALUE_TYPE_NONE,
-                                    /*propagate_delete=*/false)
+                                    DELETE_PROPAGATION_TYPE_NONE)
                        .SetCardinality(CARDINALITY_REQUIRED)))
                .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()), IsOk());
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
 }
 
 TEST_P(SchemaUtilTest,
@@ -4157,7 +4597,7 @@
                   .SetName("Foo")
                   .SetDataType(TYPE_STRING)
                   .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                               /*propagate_delete=*/false)
+                               DELETE_PROPAGATION_TYPE_NONE)
                   .SetCardinality(CARDINALITY_OPTIONAL)))
           .AddType(SchemaTypeConfigBuilder().SetType("B").AddProperty(
               PropertyConfigBuilder()
@@ -4172,7 +4612,7 @@
                                        /*index_nested_properties=*/false)
                   .SetCardinality(CARDINALITY_REPEATED)))
           .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
   // Passes once we use non-REPEATED cardinality for "C.b", i.e. the dependency
@@ -4184,7 +4624,7 @@
                        .SetName("Foo")
                        .SetDataType(TYPE_STRING)
                        .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                                    /*propagate_delete=*/false)
+                                    DELETE_PROPAGATION_TYPE_NONE)
                        .SetCardinality(CARDINALITY_OPTIONAL)))
                .AddType(SchemaTypeConfigBuilder().SetType("B").AddProperty(
                    PropertyConfigBuilder()
@@ -4199,7 +4639,8 @@
                                             /*index_nested_properties=*/false)
                        .SetCardinality(CARDINALITY_OPTIONAL)))
                .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()), IsOk());
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
 }
 
 TEST_P(
@@ -4215,7 +4656,7 @@
                   .SetName("Foo")
                   .SetDataType(TYPE_STRING)
                   .SetJoinable(JOINABLE_VALUE_TYPE_NONE,
-                               /*propagate_delete=*/false)
+                               DELETE_PROPAGATION_TYPE_NONE)
                   .SetCardinality(CARDINALITY_OPTIONAL)))
           .AddType(SchemaTypeConfigBuilder()
                        .SetType("B")
@@ -4230,7 +4671,7 @@
                                .SetName("Bar")
                                .SetDataType(TYPE_STRING)
                                .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                                            /*propagate_delete=*/false)
+                                            DELETE_PROPAGATION_TYPE_NONE)
                                .SetCardinality(CARDINALITY_OPTIONAL)))
           .AddType(SchemaTypeConfigBuilder().SetType("C").AddProperty(
               PropertyConfigBuilder()
@@ -4242,7 +4683,8 @@
 
   // Passes since nested schema type with REPEATED cardinality doesn't have
   // joinable property.
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()), IsOk());
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
 }
 
 TEST_P(SchemaUtilTest,
@@ -4261,7 +4703,7 @@
                   .SetName("Foo")
                   .SetDataType(TYPE_STRING)
                   .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                               /*propagate_delete=*/false)
+                               DELETE_PROPAGATION_TYPE_NONE)
                   .SetCardinality(CARDINALITY_OPTIONAL)))
           .AddType(SchemaTypeConfigBuilder()
                        .SetType("B")
@@ -4278,7 +4720,7 @@
                                             /*index_nested_properties=*/false)
                                         .SetCardinality(CARDINALITY_REPEATED)))
           .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
   // Passes once we use non-REPEATED cardinality for "B.a2", i.e. the dependency
@@ -4295,7 +4737,7 @@
                   .SetName("Foo")
                   .SetDataType(TYPE_STRING)
                   .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                               /*propagate_delete=*/false)
+                               DELETE_PROPAGATION_TYPE_NONE)
                   .SetCardinality(CARDINALITY_OPTIONAL)))
           .AddType(SchemaTypeConfigBuilder()
                        .SetType("B")
@@ -4312,7 +4754,8 @@
                                             /*index_nested_properties=*/false)
                                         .SetCardinality(CARDINALITY_OPTIONAL)))
           .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()), IsOk());
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
 }
 
 TEST_P(SchemaUtilTest, ValidateNestedJoinablePropertyDiamondRelationship) {
@@ -4334,7 +4777,7 @@
                   .SetName("Foo")
                   .SetDataType(TYPE_STRING)
                   .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                               /*propagate_delete=*/false)
+                               DELETE_PROPAGATION_TYPE_NONE)
                   .SetCardinality(CARDINALITY_OPTIONAL)))
           .AddType(SchemaTypeConfigBuilder().SetType("B").AddProperty(
               PropertyConfigBuilder()
@@ -4363,7 +4806,8 @@
                                             /*index_nested_properties=*/false)
                                         .SetCardinality(CARDINALITY_OPTIONAL)))
           .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()), IsOk());
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
 
   // Fails once we change any of edge to REPEATED cardinality.
   //           B
@@ -4382,7 +4826,7 @@
                   .SetName("Foo")
                   .SetDataType(TYPE_STRING)
                   .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                               /*propagate_delete=*/false)
+                               DELETE_PROPAGATION_TYPE_NONE)
                   .SetCardinality(CARDINALITY_OPTIONAL)))
           .AddType(SchemaTypeConfigBuilder().SetType("B").AddProperty(
               PropertyConfigBuilder()
@@ -4411,7 +4855,7 @@
                                             /*index_nested_properties=*/false)
                                         .SetCardinality(CARDINALITY_OPTIONAL)))
           .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
   //           B
@@ -4430,7 +4874,7 @@
                   .SetName("Foo")
                   .SetDataType(TYPE_STRING)
                   .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                               /*propagate_delete=*/false)
+                               DELETE_PROPAGATION_TYPE_NONE)
                   .SetCardinality(CARDINALITY_OPTIONAL)))
           .AddType(SchemaTypeConfigBuilder().SetType("B").AddProperty(
               PropertyConfigBuilder()
@@ -4459,7 +4903,7 @@
                                             /*index_nested_properties=*/false)
                                         .SetCardinality(CARDINALITY_OPTIONAL)))
           .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
   //           B
@@ -4478,7 +4922,7 @@
                   .SetName("Foo")
                   .SetDataType(TYPE_STRING)
                   .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                               /*propagate_delete=*/false)
+                               DELETE_PROPAGATION_TYPE_NONE)
                   .SetCardinality(CARDINALITY_OPTIONAL)))
           .AddType(SchemaTypeConfigBuilder().SetType("B").AddProperty(
               PropertyConfigBuilder()
@@ -4507,7 +4951,7 @@
                                             /*index_nested_properties=*/false)
                                         .SetCardinality(CARDINALITY_REPEATED)))
           .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
   //           B
@@ -4526,7 +4970,7 @@
                   .SetName("Foo")
                   .SetDataType(TYPE_STRING)
                   .SetJoinable(JOINABLE_VALUE_TYPE_QUALIFIED_ID,
-                               /*propagate_delete=*/false)
+                               DELETE_PROPAGATION_TYPE_NONE)
                   .SetCardinality(CARDINALITY_OPTIONAL)))
           .AddType(SchemaTypeConfigBuilder().SetType("B").AddProperty(
               PropertyConfigBuilder()
@@ -4555,7 +4999,7 @@
                                             /*index_nested_properties=*/false)
                                         .SetCardinality(CARDINALITY_OPTIONAL)))
           .Build();
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -4590,7 +5034,8 @@
       ->mutable_document_indexing_config()
       ->clear_indexable_nested_properties_list();
 
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()), IsOk());
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
 }
 
 TEST_P(SchemaUtilTest,
@@ -4624,7 +5069,8 @@
       ->mutable_document_indexing_config()
       ->clear_indexable_nested_properties_list();
 
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()), IsOk());
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
 }
 
 TEST_P(SchemaUtilTest,
@@ -4654,7 +5100,8 @@
   outerSchemaType->mutable_properties(0)
       ->mutable_document_indexing_config()
       ->set_index_nested_properties(false);
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()), IsOk());
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
 }
 
 TEST_P(SchemaUtilTest, InvalidDocumentIndexingConfigFields) {
@@ -4686,7 +5133,7 @@
       ->mutable_document_indexing_config()
       ->add_indexable_nested_properties_list("prop");
 
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -4710,7 +5157,8 @@
                                         .SetCardinality(CARDINALITY_REPEATED)))
           .Build();
 
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()), IsOk());
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
+              IsOk());
 }
 
 TEST_P(SchemaUtilTest, InvalidSelfReference) {
@@ -4727,7 +5175,7 @@
                                         .SetCardinality(CARDINALITY_OPTIONAL)))
           .Build();
 
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("Invalid cycle")));
 }
@@ -4751,7 +5199,7 @@
                                         .SetCardinality(CARDINALITY_OPTIONAL)))
           .Build();
 
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("Invalid cycle")));
 }
@@ -4783,7 +5231,7 @@
           .Build();
 
   // Two degrees of referencing: A -> B -> A
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("Invalid cycle")));
 }
@@ -4824,7 +5272,7 @@
           .Build();
 
   // Three degrees of referencing: A -> B -> C -> A
-  EXPECT_THAT(SchemaUtil::Validate(schema, GetParam()),
+  EXPECT_THAT(SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                        HasSubstr("Invalid cycle")));
 }
@@ -4844,7 +5292,7 @@
 
   SchemaProto schema = SchemaBuilder().AddType(type_a).AddType(type_b).Build();
   EXPECT_THAT(
-      SchemaUtil::Validate(schema, GetParam()),
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                HasSubstr("Property text is not present in child type")));
 }
@@ -4864,7 +5312,7 @@
 
   SchemaProto schema = SchemaBuilder().AddType(type_a).AddType(type_b).Build();
   EXPECT_THAT(
-      SchemaUtil::Validate(schema, GetParam()),
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                HasSubstr("Property text is not present in child type")));
 }
@@ -4928,8 +5376,9 @@
                            .AddType(person_type)
                            .AddType(artist_type)
                            .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, SizeIs(3));
   EXPECT_THAT(d_map["Message"],
               UnorderedElementsAre(Pair("ArtistMessage", IsEmpty())));
@@ -4995,7 +5444,7 @@
                            .AddType(artist_type)
                            .Build();
   EXPECT_THAT(
-      SchemaUtil::Validate(schema, GetParam()),
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                HasSubstr("Property person from child type ArtistMessage is not "
                          "compatible to the parent type Message.")));
@@ -5051,7 +5500,7 @@
                            .AddType(artist_type)
                            .Build();
   EXPECT_THAT(
-      SchemaUtil::Validate(schema, GetParam()),
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                HasSubstr("Property text from child type ArtistMessage is not "
                          "compatible to the parent type Message.")));
@@ -5108,7 +5557,7 @@
                            .AddType(artist_type)
                            .Build();
   EXPECT_THAT(
-      SchemaUtil::Validate(schema, GetParam()),
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                HasSubstr("Property person from child type ArtistMessage is not "
                          "compatible to the parent type Message.")));
@@ -5165,8 +5614,9 @@
                            .AddType(message_type)
                            .AddType(email_message_type)
                            .Build();
-  ICING_ASSERT_OK_AND_ASSIGN(SchemaUtil::DependentMap d_map,
-                             SchemaUtil::Validate(schema, GetParam()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      SchemaUtil::DependentMap d_map,
+      SchemaUtil::Validate(schema, *feature_flags_, GetParam()));
   EXPECT_THAT(d_map, SizeIs(2));
   EXPECT_THAT(d_map["Email"],
               UnorderedElementsAre(Pair("EmailMessage", IsEmpty())));
@@ -5222,7 +5672,7 @@
                             .AddType(email_message_type1)
                             .Build();
   EXPECT_THAT(
-      SchemaUtil::Validate(schema1, GetParam()),
+      SchemaUtil::Validate(schema1, *feature_flags_, GetParam()),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
                HasSubstr(
                    "Property sender is not present in child type EmailMessage, "
@@ -5251,7 +5701,7 @@
                             .AddType(email_message_type2)
                             .Build();
   EXPECT_THAT(
-      SchemaUtil::Validate(schema2, GetParam()),
+      SchemaUtil::Validate(schema2, *feature_flags_, GetParam()),
       StatusIs(
           libtextclassifier3::StatusCode::INVALID_ARGUMENT,
           HasSubstr(
@@ -5259,6 +5709,149 @@
               "but it is defined in the parent type Message.")));
 }
 
+TEST_P(SchemaUtilTest, ValidateScorableType_EnabledForSupportedDataTypes) {
+  SchemaTypeConfigProto type_int64 =
+      SchemaTypeConfigBuilder()
+          .SetType("A")
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("c")
+                           .SetCardinality(CARDINALITY_OPTIONAL)
+                           .SetScorableType(SCORABLE_TYPE_ENABLED)
+                           .SetDataTypeInt64(NUMERIC_MATCH_RANGE))
+          .Build();
+  EXPECT_THAT(SchemaUtil::Validate(SchemaBuilder().AddType(type_int64).Build(),
+                                   *feature_flags_, GetParam()),
+              StatusIs(libtextclassifier3::StatusCode::OK));
+
+  SchemaTypeConfigProto type_double =
+      SchemaTypeConfigBuilder()
+          .SetType("B")
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("c")
+                           .SetCardinality(CARDINALITY_OPTIONAL)
+                           .SetScorableType(SCORABLE_TYPE_ENABLED)
+                           .SetDataType(TYPE_DOUBLE))
+          .Build();
+
+  EXPECT_THAT(SchemaUtil::Validate(SchemaBuilder().AddType(type_double).Build(),
+                                   *feature_flags_, GetParam()),
+              StatusIs(libtextclassifier3::StatusCode::OK));
+
+  SchemaTypeConfigProto type_boolean =
+      SchemaTypeConfigBuilder()
+          .SetType("B")
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("c")
+                           .SetCardinality(CARDINALITY_OPTIONAL)
+                           .SetScorableType(SCORABLE_TYPE_ENABLED)
+                           .SetDataType(TYPE_BOOLEAN))
+          .Build();
+
+  EXPECT_THAT(
+      SchemaUtil::Validate(SchemaBuilder().AddType(type_boolean).Build(),
+                           *feature_flags_, GetParam()),
+      StatusIs(libtextclassifier3::StatusCode::OK));
+}
+
+TEST_P(SchemaUtilTest, ValidateScorableType_EnabledForUnsupportedDataTypes) {
+  SchemaTypeConfigProto type_a =
+      SchemaTypeConfigBuilder()
+          .SetType("A")
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("c")
+                           .SetCardinality(CARDINALITY_OPTIONAL)
+                           .SetScorableType(SCORABLE_TYPE_ENABLED)
+                           .SetDataType(TYPE_STRING))
+          .Build();
+  EXPECT_THAT(
+      SchemaUtil::Validate(SchemaBuilder().AddType(type_a).Build(),
+                           *feature_flags_, GetParam()),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+               HasSubstr("Field 'scorable_type' cannot be enabled for data "
+                         "type 'STRING' for schema property 'A.c'")));
+}
+
+TEST_P(SchemaUtilTest,
+       ValidateScorableType_CannotBeExplicitlyEnabledForDocumentType) {
+  SchemaTypeConfigProto message_type =
+      SchemaTypeConfigBuilder()
+          .SetType(kMessageType)
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("prop")
+                           .SetDataType(TYPE_INT64)
+                           .SetScorableType(SCORABLE_TYPE_ENABLED)
+                           .SetCardinality(CARDINALITY_REPEATED))
+          .Build();
+
+  SchemaTypeConfigProto type_document =
+      SchemaTypeConfigBuilder()
+          .SetType("B")
+          .AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("c")
+                  .SetCardinality(CARDINALITY_OPTIONAL)
+                  .SetScorableType(SCORABLE_TYPE_ENABLED)
+                  .SetDataTypeDocument(kMessageType,
+                                       /*index_nested_properties=*/true))
+          .Build();
+
+  EXPECT_THAT(
+      SchemaUtil::Validate(
+          SchemaBuilder().AddType(type_document).AddType(message_type).Build(),
+          *feature_flags_, GetParam()),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+               HasSubstr("Field 'scorable_type' shouldn't be explicitly "
+                         "set for data type DOCUMENT")));
+}
+
+TEST_P(SchemaUtilTest,
+       ValidateScorableType_CannotBeExplicitlyDisabledForDocumentType) {
+  SchemaTypeConfigProto message_type =
+      SchemaTypeConfigBuilder()
+          .SetType(kMessageType)
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("prop")
+                           .SetDataType(TYPE_INT64)
+                           .SetScorableType(SCORABLE_TYPE_ENABLED)
+                           .SetCardinality(CARDINALITY_REPEATED))
+          .Build();
+
+  SchemaTypeConfigProto type_document =
+      SchemaTypeConfigBuilder()
+          .SetType("B")
+          .AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("c")
+                  .SetCardinality(CARDINALITY_OPTIONAL)
+                  .SetScorableType(SCORABLE_TYPE_DISABLED)
+                  .SetDataTypeDocument(kMessageType,
+                                       /*index_nested_properties=*/true))
+          .Build();
+
+  EXPECT_THAT(
+      SchemaUtil::Validate(
+          SchemaBuilder().AddType(type_document).AddType(message_type).Build(),
+          *feature_flags_, GetParam()),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+               HasSubstr("Field 'scorable_type' shouldn't be explicitly "
+                         "set for data type DOCUMENT")));
+}
+
+TEST_P(SchemaUtilTest, ValidateScorableType_DisabledForUnsupportedDataTypes) {
+  SchemaTypeConfigProto type_b =
+      SchemaTypeConfigBuilder()
+          .SetType("A")
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("c")
+                           .SetCardinality(CARDINALITY_OPTIONAL)
+                           .SetScorableType(SCORABLE_TYPE_DISABLED)
+                           .SetDataType(TYPE_STRING))
+          .Build();
+  EXPECT_THAT(SchemaUtil::Validate(SchemaBuilder().AddType(type_b).Build(),
+                                   *feature_flags_, GetParam()),
+              StatusIs(libtextclassifier3::StatusCode::OK));
+}
+
 INSTANTIATE_TEST_SUITE_P(
     SchemaUtilTest, SchemaUtilTest,
     testing::Values(/*allow_circular_schema_definitions=*/true, false));
diff --git a/icing/schema/scorable_property_manager.cc b/icing/schema/scorable_property_manager.cc
new file mode 100644
index 0000000..440d1bd
--- /dev/null
+++ b/icing/schema/scorable_property_manager.cc
@@ -0,0 +1,135 @@
+// Copyright (C) 2024 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/schema/scorable_property_manager.h"
+
+#include <optional>
+#include <string>
+#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/legacy/core/icing-string-util.h"
+#include "icing/schema/schema-property-iterator.h"
+#include "icing/schema/schema-util.h"
+#include "icing/store/document-filter-data.h"
+#include "icing/util/status-macros.h"
+
+namespace icing {
+namespace lib {
+
+libtextclassifier3::StatusOr<std::optional<int>>
+ScorablePropertyManager::GetScorablePropertyIndex(
+    SchemaTypeId schema_type_id, std::string_view property_path,
+    const SchemaUtil::TypeConfigMap& type_config_map,
+    const std::unordered_map<SchemaTypeId, std::string>&
+        schema_id_to_type_map) {
+  ICING_ASSIGN_OR_RETURN(auto cache_iter, LookupAndMaybeUpdateCache(
+                                              schema_type_id, type_config_map,
+                                              schema_id_to_type_map));
+  auto iter =
+      cache_iter->second.property_path_to_index_map.find(property_path.data());
+  if (iter == cache_iter->second.property_path_to_index_map.end()) {
+    return std::nullopt;
+  }
+  return iter->second;
+}
+
+libtextclassifier3::StatusOr<
+    const std::vector<ScorablePropertyManager::ScorablePropertyInfo>*>
+ScorablePropertyManager::GetOrderedScorablePropertyInfo(
+    SchemaTypeId schema_type_id,
+    const SchemaUtil::TypeConfigMap& type_config_map,
+    const std::unordered_map<SchemaTypeId, std::string>&
+        schema_id_to_type_map) {
+  ICING_ASSIGN_OR_RETURN(auto cache_iter, LookupAndMaybeUpdateCache(
+                                              schema_type_id, type_config_map,
+                                              schema_id_to_type_map));
+  return &cache_iter->second.ordered_scorable_property_info;
+}
+
+libtextclassifier3::StatusOr<std::unordered_map<
+    SchemaTypeId,
+    ScorablePropertyManager::DerivedScorablePropertySchema>::iterator>
+ScorablePropertyManager::LookupAndMaybeUpdateCache(
+    SchemaTypeId schema_type_id,
+    const SchemaUtil::TypeConfigMap& type_config_map,
+    const std::unordered_map<SchemaTypeId, std::string>&
+        schema_id_to_type_map) {
+  auto cache_iter = scorable_property_schema_cache_.find(schema_type_id);
+  if (cache_iter == scorable_property_schema_cache_.end()) {
+    if (UpdateCache(schema_type_id, type_config_map, schema_id_to_type_map)) {
+      cache_iter = scorable_property_schema_cache_.find(schema_type_id);
+    } else {
+      // schema type id not found neither in the cache nor in the schema
+      // config.
+      return absl_ports::InvalidArgumentError(IcingStringUtil::StringPrintf(
+          "LookupAndMaybeUpdateCache failed: Schema type id %d not found",
+          schema_type_id));
+    }
+  }
+  return cache_iter;
+}
+
+bool ScorablePropertyManager::UpdateCache(
+    SchemaTypeId schema_type_id,
+    const SchemaUtil::TypeConfigMap& type_config_map,
+    const std::unordered_map<SchemaTypeId, std::string>&
+        schema_id_to_type_map) {
+  auto schema_id_iter = schema_id_to_type_map.find(schema_type_id);
+  if (schema_id_iter == schema_id_to_type_map.end()) {
+    return false;
+  }
+  auto schema_config_iter = type_config_map.find(schema_id_iter->second);
+  if (schema_config_iter == type_config_map.end()) {
+    return false;
+  }
+  std::vector<ScorablePropertyInfo> property_info_vector;
+  std::unordered_map<std::string, int> index_map;
+
+  SchemaPropertyIterator iterator(schema_config_iter->second, type_config_map);
+  while (true) {
+    libtextclassifier3::Status status = iterator.Advance();
+    if (!status.ok()) {
+      if (absl_ports::IsOutOfRange(status)) {
+        break;
+      }
+      // Swallow other type of errors which should not happen if the schema has
+      // been validated already.
+      return false;
+    }
+    if (iterator.GetCurrentPropertyConfig().scorable_type() ==
+        PropertyConfigProto::ScorableType::ENABLED) {
+      index_map[iterator.GetCurrentPropertyPath()] =
+          property_info_vector.size();
+      ScorablePropertyInfo scorable_property_info;
+      scorable_property_info.property_path = iterator.GetCurrentPropertyPath();
+      scorable_property_info.data_type =
+          iterator.GetCurrentPropertyConfig().data_type();
+      property_info_vector.push_back(std::move(scorable_property_info));
+    }
+  }
+  scorable_property_schema_cache_.insert(
+      {schema_type_id,
+       DerivedScorablePropertySchema(std::move(property_info_vector),
+                                     std::move(index_map))});
+  return true;
+}
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/schema/scorable_property_manager.h b/icing/schema/scorable_property_manager.h
new file mode 100644
index 0000000..0ea1057
--- /dev/null
+++ b/icing/schema/scorable_property_manager.h
@@ -0,0 +1,126 @@
+// Copyright (C) 2024 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_SCHEMA_SCORABLE_PROPERTY_MANAGER_H_
+#define ICING_SCHEMA_SCORABLE_PROPERTY_MANAGER_H_
+
+#include <optional>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/schema/schema-util.h"
+#include "icing/store/document-filter-data.h"
+
+namespace icing {
+namespace lib {
+
+// This class serves as a cache layer to access schema configs for scorable
+// properties, with the schema-store being the source of truth.
+class ScorablePropertyManager {
+ public:
+  struct ScorablePropertyInfo {
+    // Scorable property's property_path in the schema config, eg:
+    // "property_name", "parent_schema.nested_child_schema.property_name".
+    std::string property_path;
+    // Data type of the property.
+    PropertyConfigProto::DataType::Code data_type;
+  };
+
+  ScorablePropertyManager() = default;
+
+  // Delete copy constructor and assignment operator.
+  ScorablePropertyManager(const ScorablePropertyManager&) = delete;
+  ScorablePropertyManager& operator=(const ScorablePropertyManager&) = delete;
+
+  // Gets the index of the given |property_path|, where the index N means that
+  // it is the Nth scorable property path in the schema config of the given
+  // |schema_type_id|, in lexicographical order.
+  //
+  // Returns:
+  //   - Index on success
+  //   - std::nullopt if the |property_path| doesnn't point to a scorable
+  //     property under the |schema_type_id|
+  //   - INVALID_ARGUMENT if |schema_type_id| is invalid
+  libtextclassifier3::StatusOr<std::optional<int>> GetScorablePropertyIndex(
+      SchemaTypeId schema_type_id, std::string_view property_path,
+      const SchemaUtil::TypeConfigMap& type_config_map,
+      const std::unordered_map<SchemaTypeId, std::string>&
+          schema_id_to_type_map);
+
+  // Returns the list of ScorablePropertyInfo for the given |schema_type_id|,
+  // in lexicographical order of its property path.
+  //
+  // Returns:
+  //   - Vector of scorable property info on success. The vector can be empty
+  //     if no scorable property is found under the schema config of
+  //     |schema_type_id|.
+  //   - INVALID_ARGUMENT if |schema_type_id| is invalid.
+  libtextclassifier3::StatusOr<const std::vector<ScorablePropertyInfo>*>
+  GetOrderedScorablePropertyInfo(
+      SchemaTypeId schema_type_id,
+      const SchemaUtil::TypeConfigMap& type_config_map,
+      const std::unordered_map<SchemaTypeId, std::string>&
+          schema_id_to_type_map);
+
+ private:
+  struct DerivedScorablePropertySchema {
+    // vector of ScorablePropertyInfo, in lexicographical order of its property
+    // path.
+    std::vector<ScorablePropertyInfo> ordered_scorable_property_info;
+    // Map of scorable property path to its index, where index N means that the
+    // property is the Nth scorable property in the schema config, in
+    // lexicographical order.
+    std::unordered_map<std::string, int> property_path_to_index_map;
+
+    explicit DerivedScorablePropertySchema(
+        std::vector<ScorablePropertyInfo>&& ordered_properties,
+        std::unordered_map<std::string, int>&& index_map)
+        : ordered_scorable_property_info(std::move(ordered_properties)),
+          property_path_to_index_map(std::move(index_map)) {}
+  };
+
+  // Looks up the entry in |scorable_property_schema_cache_| for the key
+  // |schema_type_id|. If the entry is not found, try to update the cache and
+  // return the iterator of the updated entry.
+  //
+  // Returns:
+  //   - INVALID_ARGUMENT if |schema_type_id| is not found from the schema.
+  libtextclassifier3::StatusOr<
+      std::unordered_map<SchemaTypeId, DerivedScorablePropertySchema>::iterator>
+  LookupAndMaybeUpdateCache(SchemaTypeId schema_type_id,
+                            const SchemaUtil::TypeConfigMap& type_config_map,
+                            const std::unordered_map<SchemaTypeId, std::string>&
+                                schema_id_to_type_map);
+
+  // Updates the entry in |scorable_property_schema_cache_| for the key
+  // |schema_type_id|.
+  //
+  // Returns true if an entry is successfully inserted to the cache.
+  bool UpdateCache(SchemaTypeId schema_type_id,
+                   const SchemaUtil::TypeConfigMap& type_config_map,
+                   const std::unordered_map<SchemaTypeId, std::string>&
+                       schema_id_to_type_map);
+
+  std::unordered_map<SchemaTypeId, DerivedScorablePropertySchema>
+      scorable_property_schema_cache_;
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_SCHEMA_SCORABLE_PROPERTY_MANAGER_H_
diff --git a/icing/schema/scorable_property_manager_test.cc b/icing/schema/scorable_property_manager_test.cc
new file mode 100644
index 0000000..0cbc6d8
--- /dev/null
+++ b/icing/schema/scorable_property_manager_test.cc
@@ -0,0 +1,404 @@
+// Copyright (C) 2024 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/schema/scorable_property_manager.h"
+
+#include <optional>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "icing/proto/schema.pb.h"
+#include "icing/schema-builder.h"
+#include "icing/schema/schema-util.h"
+#include "icing/store/document-filter-data.h"
+#include "icing/testing/common-matchers.h"
+
+namespace icing {
+namespace lib {
+
+namespace {
+
+using ::testing::ElementsAre;
+using ::testing::Eq;
+using ::testing::Pointee;
+
+constexpr int kEmailSchemaTypeId = 1;
+constexpr int kMessageSchemaTypeId = 2;
+constexpr int kPersonSchemaTypeId = 3;
+
+SchemaTypeConfigProto CreateEmailTypeConfig() {
+  return SchemaTypeConfigBuilder()
+      .SetType("email")
+      .AddProperty(PropertyConfigBuilder()
+                       .SetName("subject")
+                       .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                       .SetCardinality(CARDINALITY_OPTIONAL))
+      .AddProperty(PropertyConfigBuilder()
+                       .SetName("scorableInt64")
+                       .SetDataType(TYPE_INT64)
+                       .SetScorableType(SCORABLE_TYPE_ENABLED)
+                       .SetCardinality(CARDINALITY_REPEATED))
+      .AddProperty(PropertyConfigBuilder()
+                       .SetName("scorableDouble")
+                       .SetDataType(TYPE_DOUBLE)
+                       .SetScorableType(SCORABLE_TYPE_ENABLED)
+                       .SetCardinality(CARDINALITY_REPEATED))
+      .Build();
+}
+
+SchemaTypeConfigProto CreateMessageTypeConfig() {
+  return SchemaTypeConfigBuilder()
+      .SetType("message")
+      .AddProperty(PropertyConfigBuilder()
+                       .SetName("message")
+                       .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                       .SetCardinality(CARDINALITY_OPTIONAL))
+      .AddProperty(PropertyConfigBuilder()
+                       .SetName("scorableBoolean")
+                       .SetDataType(TYPE_BOOLEAN)
+                       .SetScorableType(SCORABLE_TYPE_ENABLED)
+                       .SetCardinality(CARDINALITY_REPEATED))
+      .AddProperty(PropertyConfigBuilder()
+                       .SetName("scorableDouble")
+                       .SetDataType(TYPE_DOUBLE)
+                       .SetScorableType(SCORABLE_TYPE_ENABLED)
+                       .SetCardinality(CARDINALITY_REPEATED))
+      .Build();
+}
+
+SchemaTypeConfigProto CreatePersonTypeConfig() {
+  return SchemaTypeConfigBuilder()
+      .SetType("person")
+      .AddProperty(PropertyConfigBuilder()
+                       .SetName("name")
+                       .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                       .SetCardinality(CARDINALITY_OPTIONAL))
+      .Build();
+}
+
+class ScorablePropertyManagerTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    type_config_map_.emplace("email", CreateEmailTypeConfig());
+    type_config_map_.emplace("message", CreateMessageTypeConfig());
+    type_config_map_.emplace("person", CreatePersonTypeConfig());
+    schema_id_to_type_map_.emplace(kEmailSchemaTypeId, "email");
+    schema_id_to_type_map_.emplace(kMessageSchemaTypeId, "message");
+    schema_id_to_type_map_.emplace(kPersonSchemaTypeId, "person");
+  }
+
+  SchemaUtil::TypeConfigMap type_config_map_;
+  std::unordered_map<SchemaTypeId, std::string> schema_id_to_type_map_;
+};
+
+TEST_F(ScorablePropertyManagerTest,
+       GetScorablePropertyIndex_InvalidSchemaTypeId) {
+  ScorablePropertyManager scorable_property_manager;
+
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  /*schema_type_id=*/100, "subject", type_config_map_,
+                  schema_id_to_type_map_),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+TEST_F(ScorablePropertyManagerTest,
+       GetScorablePropertyIndex_InvalidPropertyName) {
+  ScorablePropertyManager scorable_property_manager;
+
+  // non-existing property
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  kEmailSchemaTypeId, "non_existing", type_config_map_,
+                  schema_id_to_type_map_),
+              IsOkAndHolds(Eq(std::nullopt)));
+
+  // non-scorable property
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  kEmailSchemaTypeId, "subject", type_config_map_,
+                  schema_id_to_type_map_),
+              IsOkAndHolds(Eq(std::nullopt)));
+}
+
+TEST_F(ScorablePropertyManagerTest, GetScorablePropertyIndex_Ok) {
+  ScorablePropertyManager scorable_property_manager;
+
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  kEmailSchemaTypeId, "scorableDouble", type_config_map_,
+                  schema_id_to_type_map_),
+              IsOkAndHolds(0));
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  kEmailSchemaTypeId, "scorableInt64", type_config_map_,
+                  schema_id_to_type_map_),
+              IsOkAndHolds(1));
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  kMessageSchemaTypeId, "scorableBoolean", type_config_map_,
+                  schema_id_to_type_map_),
+              IsOkAndHolds(0));
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  kMessageSchemaTypeId, "scorableDouble", type_config_map_,
+                  schema_id_to_type_map_),
+              IsOkAndHolds(1));
+
+  // Repeat those calls to test the cache hits scenarios.
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  kEmailSchemaTypeId, "scorableDouble", type_config_map_,
+                  schema_id_to_type_map_),
+              IsOkAndHolds(0));
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  kEmailSchemaTypeId, "scorableInt64", type_config_map_,
+                  schema_id_to_type_map_),
+              IsOkAndHolds(1));
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  kMessageSchemaTypeId, "scorableBoolean", type_config_map_,
+                  schema_id_to_type_map_),
+              IsOkAndHolds(0));
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  kMessageSchemaTypeId, "scorableDouble", type_config_map_,
+                  schema_id_to_type_map_),
+              IsOkAndHolds(1));
+}
+
+TEST_F(ScorablePropertyManagerTest,
+       GetOrderedScorablePropertyInfo_InvalidSchemaTypeId) {
+  ScorablePropertyManager scorable_property_manager;
+
+  EXPECT_THAT(
+      scorable_property_manager.GetOrderedScorablePropertyInfo(
+          /*schema_type_id=*/100, type_config_map_, schema_id_to_type_map_),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+TEST_F(ScorablePropertyManagerTest,
+       GetOrderedScorablePropertyInfo_NoScorablePropertyInSchema) {
+  ScorablePropertyManager scorable_property_manager;
+
+  EXPECT_THAT(
+      scorable_property_manager.GetOrderedScorablePropertyInfo(
+          kPersonSchemaTypeId, type_config_map_, schema_id_to_type_map_),
+      IsOkAndHolds(Pointee(ElementsAre())));
+
+  // Repeat the call to test the cache hits scenarios.
+  EXPECT_THAT(
+      scorable_property_manager.GetOrderedScorablePropertyInfo(
+          kPersonSchemaTypeId, type_config_map_, schema_id_to_type_map_),
+      IsOkAndHolds(Pointee(ElementsAre())));
+}
+
+TEST_F(ScorablePropertyManagerTest, GetOrderedScorablePropertyInfo_Ok) {
+  ScorablePropertyManager scorable_property_manager;
+
+  EXPECT_THAT(scorable_property_manager.GetOrderedScorablePropertyInfo(
+                  kEmailSchemaTypeId, type_config_map_, schema_id_to_type_map_),
+              IsOkAndHolds(Pointee(ElementsAre(
+                  EqualsScorablePropertyInfo("scorableDouble", TYPE_DOUBLE),
+                  EqualsScorablePropertyInfo("scorableInt64", TYPE_INT64)))));
+  EXPECT_THAT(
+      scorable_property_manager.GetOrderedScorablePropertyInfo(
+          kMessageSchemaTypeId, type_config_map_, schema_id_to_type_map_),
+      IsOkAndHolds(Pointee(ElementsAre(
+          EqualsScorablePropertyInfo("scorableBoolean", TYPE_BOOLEAN),
+          EqualsScorablePropertyInfo("scorableDouble", TYPE_DOUBLE)))));
+
+  // Repeat those calls to test the cache hits scenarios.
+  EXPECT_THAT(scorable_property_manager.GetOrderedScorablePropertyInfo(
+                  kEmailSchemaTypeId, type_config_map_, schema_id_to_type_map_),
+              IsOkAndHolds(Pointee(ElementsAre(
+                  EqualsScorablePropertyInfo("scorableDouble", TYPE_DOUBLE),
+                  EqualsScorablePropertyInfo("scorableInt64", TYPE_INT64)))));
+  EXPECT_THAT(
+      scorable_property_manager.GetOrderedScorablePropertyInfo(
+          kMessageSchemaTypeId, type_config_map_, schema_id_to_type_map_),
+      IsOkAndHolds(Pointee(ElementsAre(
+          EqualsScorablePropertyInfo("scorableBoolean", TYPE_BOOLEAN),
+          EqualsScorablePropertyInfo("scorableDouble", TYPE_DOUBLE)))));
+}
+
+TEST_F(ScorablePropertyManagerTest,
+       GetOrderedScorablePropertyInfo_WithNestedSchemas) {
+  std::string person_schema_name = "Person";
+  std::string gmail_schema_name = "Gmail";
+  std::string interaction_log_schema_name = "InteractionLog";
+
+  SchemaTypeConfigProto person_schema_config =
+      SchemaTypeConfigBuilder()
+          .SetType(person_schema_name)
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("name")
+                           .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                           .SetCardinality(CARDINALITY_OPTIONAL))
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("networth")
+                           .SetDataType(TYPE_DOUBLE)
+                           .SetScorableType(SCORABLE_TYPE_ENABLED)
+                           .SetCardinality(CARDINALITY_OPTIONAL))
+          .Build();
+  SchemaTypeConfigProto gmail_schema_config =
+      SchemaTypeConfigBuilder()
+          .SetType(gmail_schema_name)
+          .AddProperty(
+              PropertyConfigBuilder().SetName("subject").SetDataTypeString(
+                  TERM_MATCH_UNKNOWN, TOKENIZER_NONE))
+          .AddProperty(
+              PropertyConfigBuilder().SetName("sender").SetDataTypeDocument(
+                  person_schema_name,
+                  /*index_nested_properties=*/true))
+          .AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("recipient")
+                  .SetDataTypeDocument(person_schema_name,
+                                       /*index_nested_properties=*/true))
+          .Build();
+  SchemaTypeConfigProto interaction_log_schema_config =
+      SchemaTypeConfigBuilder()
+          .SetType(interaction_log_schema_name)
+          .AddProperty(
+              PropertyConfigBuilder().SetName("summary").SetDataTypeString(
+                  TERM_MATCH_EXACT, TOKENIZER_PLAIN))
+          .AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("contactGmail")
+                  .SetDataTypeDocument(gmail_schema_name,
+                                       /*index_nested_properties=*/true))
+          .AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("mostContactedPerson")
+                  .SetDataTypeDocument(person_schema_name,
+                                       /*index_nested_properties=*/true))
+          .Build();
+  SchemaUtil::TypeConfigMap type_config_map = {
+      {person_schema_name, person_schema_config},
+      {gmail_schema_name, gmail_schema_config},
+      {interaction_log_schema_name, interaction_log_schema_config}};
+  std::unordered_map<SchemaTypeId, std::string> schema_id_to_type_map = {
+      {1, person_schema_name},
+      {2, gmail_schema_name},
+      {3, interaction_log_schema_name}};
+  ScorablePropertyManager scorable_property_manager;
+
+  EXPECT_THAT(scorable_property_manager.GetOrderedScorablePropertyInfo(
+                  /*schema_type_id=*/1, type_config_map, schema_id_to_type_map),
+              IsOkAndHolds(Pointee(ElementsAre(
+                  EqualsScorablePropertyInfo("networth", TYPE_DOUBLE)))));
+  EXPECT_THAT(
+      scorable_property_manager.GetOrderedScorablePropertyInfo(
+          /*schema_type_id=*/2, type_config_map, schema_id_to_type_map),
+      IsOkAndHolds(Pointee(ElementsAre(
+          EqualsScorablePropertyInfo("recipient.networth", TYPE_DOUBLE),
+          EqualsScorablePropertyInfo("sender.networth", TYPE_DOUBLE)))));
+  EXPECT_THAT(scorable_property_manager.GetOrderedScorablePropertyInfo(
+                  /*schema_type_id=*/3, type_config_map, schema_id_to_type_map),
+              IsOkAndHolds(Pointee(ElementsAre(
+                  EqualsScorablePropertyInfo("contactGmail.recipient.networth",
+                                             TYPE_DOUBLE),
+                  EqualsScorablePropertyInfo("contactGmail.sender.networth",
+                                             TYPE_DOUBLE),
+                  EqualsScorablePropertyInfo("mostContactedPerson.networth",
+                                             TYPE_DOUBLE)))));
+}
+
+TEST_F(ScorablePropertyManagerTest,
+       GetScorablePropertyIndex_WithNestedSchemas) {
+  std::string person_schema_name = "Person";
+  std::string gmail_schema_name = "Gmail";
+  std::string interaction_log_schema_name = "InteractionLog";
+
+  SchemaTypeConfigProto person_schema_config =
+      SchemaTypeConfigBuilder()
+          .SetType(person_schema_name)
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("name")
+                           .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                           .SetCardinality(CARDINALITY_OPTIONAL))
+          .AddProperty(PropertyConfigBuilder()
+                           .SetName("networth")
+                           .SetDataType(TYPE_DOUBLE)
+                           .SetScorableType(SCORABLE_TYPE_ENABLED)
+                           .SetCardinality(CARDINALITY_OPTIONAL))
+          .Build();
+  SchemaTypeConfigProto gmail_schema_config =
+      SchemaTypeConfigBuilder()
+          .SetType(gmail_schema_name)
+          .AddProperty(
+              PropertyConfigBuilder().SetName("subject").SetDataTypeString(
+                  TERM_MATCH_UNKNOWN, TOKENIZER_NONE))
+          .AddProperty(
+              PropertyConfigBuilder().SetName("sender").SetDataTypeDocument(
+                  person_schema_name,
+                  /*index_nested_properties=*/true))
+          .AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("recipient")
+                  .SetDataTypeDocument(person_schema_name,
+                                       /*index_nested_properties=*/true))
+          .Build();
+  SchemaTypeConfigProto interaction_log_schema_config =
+      SchemaTypeConfigBuilder()
+          .SetType(interaction_log_schema_name)
+          .AddProperty(
+              PropertyConfigBuilder().SetName("summary").SetDataTypeString(
+                  TERM_MATCH_EXACT, TOKENIZER_PLAIN))
+          .AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("contactGmail")
+                  .SetDataTypeDocument(gmail_schema_name,
+                                       /*index_nested_properties=*/true))
+          .AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("mostContactedPerson")
+                  .SetDataTypeDocument(person_schema_name,
+                                       /*index_nested_properties=*/true))
+          .Build();
+  SchemaUtil::TypeConfigMap type_config_map = {
+      {person_schema_name, person_schema_config},
+      {gmail_schema_name, gmail_schema_config},
+      {interaction_log_schema_name, interaction_log_schema_config}};
+  std::unordered_map<SchemaTypeId, std::string> schema_id_to_type_map = {
+      {1, person_schema_name},
+      {2, gmail_schema_name},
+      {3, interaction_log_schema_name}};
+  ScorablePropertyManager scorable_property_manager;
+
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  /*schema_type_id=*/1, "networth", type_config_map,
+                  schema_id_to_type_map),
+              IsOkAndHolds(0));
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  /*schema_type_id=*/2, "recipient.networth", type_config_map,
+                  schema_id_to_type_map),
+              IsOkAndHolds(0));
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  /*schema_type_id=*/2, "sender.networth", type_config_map,
+                  schema_id_to_type_map),
+              IsOkAndHolds(1));
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  /*schema_type_id=*/3, "contactGmail.recipient.networth",
+                  type_config_map, schema_id_to_type_map),
+              IsOkAndHolds(0));
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  /*schema_type_id=*/3, "contactGmail.sender.networth",
+                  type_config_map, schema_id_to_type_map),
+              IsOkAndHolds(1));
+  EXPECT_THAT(scorable_property_manager.GetScorablePropertyIndex(
+                  /*schema_type_id=*/3, "mostContactedPerson.networth",
+                  type_config_map, schema_id_to_type_map),
+              IsOkAndHolds(2));
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/schema/section-manager.cc b/icing/schema/section-manager.cc
index 8689bf2..7097a17 100644
--- a/icing/schema/section-manager.cc
+++ b/icing/schema/section-manager.cc
@@ -62,6 +62,7 @@
       property_config.string_indexing_config().term_match_type(),
       property_config.integer_indexing_config().numeric_match_type(),
       property_config.embedding_indexing_config().embedding_indexing_type(),
+      property_config.embedding_indexing_config().quantization_type(),
       std::move(concatenated_path)));
   return libtextclassifier3::Status::OK;
 }
diff --git a/icing/schema/section-manager_test.cc b/icing/schema/section-manager_test.cc
index b735fb1..62c2528 100644
--- a/icing/schema/section-manager_test.cc
+++ b/icing/schema/section-manager_test.cc
@@ -55,6 +55,8 @@
 static constexpr std::string_view kPropertySubject = "subject";
 static constexpr std::string_view kPropertySubjectEmbedding =
     "subjectEmbedding";
+static constexpr std::string_view kPropertySubjectEmbeddingQuantized =
+    "subjectEmbeddingQuantized";
 static constexpr std::string_view kPropertyTimestamp = "timestamp";
 // non-indexable
 static constexpr std::string_view kPropertyAttachment = "attachment";
@@ -124,6 +126,15 @@
       .Build();
 }
 
+PropertyConfigProto CreateSubjectEmbeddingQuantizedPropertyConfig() {
+  return PropertyConfigBuilder()
+      .SetName(kPropertySubjectEmbeddingQuantized)
+      .SetDataTypeVector(EMBEDDING_INDEXING_LINEAR_SEARCH,
+                         QUANTIZATION_TYPE_QUANTIZE_8_BIT)
+      .SetCardinality(CARDINALITY_OPTIONAL)
+      .Build();
+}
+
 PropertyConfigProto CreateTimestampPropertyConfig() {
   return PropertyConfigBuilder()
       .SetName(kPropertyTimestamp)
@@ -161,6 +172,7 @@
       .SetType(kTypeEmail)
       .AddProperty(CreateSubjectPropertyConfig())
       .AddProperty(CreateSubjectEmbeddingPropertyConfig())
+      .AddProperty(CreateSubjectEmbeddingQuantizedPropertyConfig())
       .AddProperty(PropertyConfigBuilder()
                        .SetName(kPropertyText)
                        .SetDataTypeString(TERM_MATCH_UNKNOWN, TOKENIZER_NONE)
@@ -249,6 +261,8 @@
             .AddStringProperty(std::string(kPropertySubject), "the subject")
             .AddVectorProperty(std::string(kPropertySubjectEmbedding),
                                email_subject_embedding_)
+            .AddVectorProperty(std::string(kPropertySubjectEmbeddingQuantized),
+                               email_subject_embedding_)
             .AddStringProperty(std::string(kPropertyText), "the text")
             .AddBytesProperty(std::string(kPropertyAttachment),
                               "attachment bytes")
@@ -333,14 +347,14 @@
   EXPECT_THAT(section_group.integer_sections[0].content, ElementsAre(1, 2, 3));
 
   EXPECT_THAT(section_group.integer_sections[1].metadata,
-              EqualsSectionMetadata(/*expected_id=*/4,
+              EqualsSectionMetadata(/*expected_id=*/5,
                                     /*expected_property_path=*/"timestamp",
                                     CreateTimestampPropertyConfig()));
   EXPECT_THAT(section_group.integer_sections[1].content,
               ElementsAre(kDefaultTimestamp));
 
   // Vector sections
-  EXPECT_THAT(section_group.vector_sections, SizeIs(1));
+  EXPECT_THAT(section_group.vector_sections, SizeIs(2));
   EXPECT_THAT(
       section_group.vector_sections[0].metadata,
       EqualsSectionMetadata(/*expected_id=*/3,
@@ -348,6 +362,13 @@
                             CreateSubjectEmbeddingPropertyConfig()));
   EXPECT_THAT(section_group.vector_sections[0].content,
               ElementsAre(EqualsProto(email_subject_embedding_)));
+  EXPECT_THAT(section_group.vector_sections[1].metadata,
+              EqualsSectionMetadata(
+                  /*expected_id=*/4,
+                  /*expected_property_path=*/"subjectEmbeddingQuantized",
+                  CreateSubjectEmbeddingQuantizedPropertyConfig()));
+  EXPECT_THAT(section_group.vector_sections[1].content,
+              ElementsAre(EqualsProto(email_subject_embedding_)));
 }
 
 TEST_F(SectionManagerTest, ExtractSectionsNested) {
@@ -394,14 +415,14 @@
 
   EXPECT_THAT(
       section_group.integer_sections[1].metadata,
-      EqualsSectionMetadata(/*expected_id=*/4,
+      EqualsSectionMetadata(/*expected_id=*/5,
                             /*expected_property_path=*/"emails.timestamp",
                             CreateTimestampPropertyConfig()));
   EXPECT_THAT(section_group.integer_sections[1].content,
               ElementsAre(kDefaultTimestamp, kDefaultTimestamp));
 
   // Vector sections
-  EXPECT_THAT(section_group.vector_sections, SizeIs(1));
+  EXPECT_THAT(section_group.vector_sections, SizeIs(2));
   EXPECT_THAT(section_group.vector_sections[0].metadata,
               EqualsSectionMetadata(
                   /*expected_id=*/3,
@@ -410,6 +431,14 @@
   EXPECT_THAT(section_group.vector_sections[0].content,
               ElementsAre(EqualsProto(email_subject_embedding_),
                           EqualsProto(email_subject_embedding_)));
+  EXPECT_THAT(section_group.vector_sections[1].metadata,
+              EqualsSectionMetadata(
+                  /*expected_id=*/4,
+                  /*expected_property_path=*/"emails.subjectEmbeddingQuantized",
+                  CreateSubjectEmbeddingQuantizedPropertyConfig()));
+  EXPECT_THAT(section_group.vector_sections[1].content,
+              ElementsAre(EqualsProto(email_subject_embedding_),
+                          EqualsProto(email_subject_embedding_)));
 }
 
 TEST_F(SectionManagerTest, ExtractSectionsIndexableNestedPropertiesList) {
@@ -508,7 +537,8 @@
   //   1 -> recipients
   //   2 -> subject
   //   3 -> subjectEmbedding
-  //   4 -> timestamp
+  //   4 -> subjectEmbeddingQuantized
+  //   5 -> timestamp
   EXPECT_THAT(schema_type_manager->section_manager().GetSectionMetadata(
                   /*schema_type_id=*/0, /*section_id=*/0),
               IsOkAndHolds(Pointee(EqualsSectionMetadata(
@@ -533,7 +563,13 @@
   EXPECT_THAT(schema_type_manager->section_manager().GetSectionMetadata(
                   /*schema_type_id=*/0, /*section_id=*/4),
               IsOkAndHolds(Pointee(EqualsSectionMetadata(
-                  /*expected_id=*/4, /*expected_property_path=*/"timestamp",
+                  /*expected_id=*/4,
+                  /*expected_property_path=*/"subjectEmbeddingQuantized",
+                  CreateSubjectEmbeddingQuantizedPropertyConfig()))));
+  EXPECT_THAT(schema_type_manager->section_manager().GetSectionMetadata(
+                  /*schema_type_id=*/0, /*section_id=*/5),
+              IsOkAndHolds(Pointee(EqualsSectionMetadata(
+                  /*expected_id=*/5, /*expected_property_path=*/"timestamp",
                   CreateTimestampPropertyConfig()))));
 
   // Conversation (section id -> section property path):
@@ -541,8 +577,9 @@
   //   1 -> emails.recipients
   //   2 -> emails.subject
   //   3 -> emails.subjectEmbedding
-  //   4 -> emails.timestamp
-  //   5 -> name
+  //   4 -> emails.subjectEmbeddingQuantized
+  //   5 -> emails.timestamp
+  //   6 -> name
   EXPECT_THAT(
       schema_type_manager->section_manager().GetSectionMetadata(
           /*schema_type_id=*/1, /*section_id=*/0),
@@ -567,16 +604,22 @@
                   /*expected_id=*/3,
                   /*expected_property_path=*/"emails.subjectEmbedding",
                   CreateSubjectEmbeddingPropertyConfig()))));
+  EXPECT_THAT(schema_type_manager->section_manager().GetSectionMetadata(
+                  /*schema_type_id=*/1, /*section_id=*/4),
+              IsOkAndHolds(Pointee(EqualsSectionMetadata(
+                  /*expected_id=*/4,
+                  /*expected_property_path=*/"emails.subjectEmbeddingQuantized",
+                  CreateSubjectEmbeddingQuantizedPropertyConfig()))));
   EXPECT_THAT(
       schema_type_manager->section_manager().GetSectionMetadata(
-          /*schema_type_id=*/1, /*section_id=*/4),
+          /*schema_type_id=*/1, /*section_id=*/5),
       IsOkAndHolds(Pointee(EqualsSectionMetadata(
-          /*expected_id=*/4, /*expected_property_path=*/"emails.timestamp",
+          /*expected_id=*/5, /*expected_property_path=*/"emails.timestamp",
           CreateTimestampPropertyConfig()))));
   EXPECT_THAT(schema_type_manager->section_manager().GetSectionMetadata(
-                  /*schema_type_id=*/1, /*section_id=*/5),
+                  /*schema_type_id=*/1, /*section_id=*/6),
               IsOkAndHolds(Pointee(EqualsSectionMetadata(
-                  /*expected_id=*/5, /*expected_property_path=*/"name",
+                  /*expected_id=*/6, /*expected_property_path=*/"name",
                   CreateNamePropertyConfig()))));
 
   // Group (section id -> section property path):
@@ -686,12 +729,13 @@
   //   1 -> recipients
   //   2 -> subject
   //   3 -> subjectEmbedding
-  //   4 -> timestamp
+  //   4 -> subjectEmbeddingQuantized
+  //   5 -> timestamp
   EXPECT_THAT(schema_type_manager->section_manager().GetSectionMetadata(
                   /*schema_type_id=*/0, /*section_id=*/-1),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(schema_type_manager->section_manager().GetSectionMetadata(
-                  /*schema_type_id=*/0, /*section_id=*/5),
+                  /*schema_type_id=*/0, /*section_id=*/6),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
   // Conversation (section id -> section property path):
@@ -699,13 +743,14 @@
   //   1 -> emails.recipients
   //   2 -> emails.subject
   //   3 -> emails.subjectEmbedding
-  //   4 -> emails.timestamp
-  //   5 -> name
+  //   4 -> subjectEmbeddingQuantized
+  //   5 -> emails.timestamp
+  //   6 -> name
   EXPECT_THAT(schema_type_manager->section_manager().GetSectionMetadata(
                   /*schema_type_id=*/1, /*section_id=*/-1),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(schema_type_manager->section_manager().GetSectionMetadata(
-                  /*schema_type_id=*/1, /*section_id=*/6),
+                  /*schema_type_id=*/1, /*section_id=*/7),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
diff --git a/icing/schema/section.h b/icing/schema/section.h
index 76e9e57..c866351 100644
--- a/icing/schema/section.h
+++ b/icing/schema/section.h
@@ -100,6 +100,15 @@
   //   Contents will be indexed for linear search.
   EmbeddingIndexingConfig::EmbeddingIndexingType::Code embedding_indexing_type;
 
+  // How vectors in a vector section should be quantized.
+  //
+  // QuantizationType::NONE:
+  //   Contents will not be quantized.
+  //
+  // QuantizationType::QUANTIZE_8_BIT:
+  //   Contents will be quantized to 8 bits.
+  EmbeddingIndexingConfig::QuantizationType::Code quantization_type;
+
   explicit SectionMetadata(
       SectionId id_in, PropertyConfigProto::DataType::Code data_type_in,
       StringIndexingConfig::TokenizerType::Code tokenizer,
@@ -107,6 +116,7 @@
       IntegerIndexingConfig::NumericMatchType::Code numeric_match_type_in,
       EmbeddingIndexingConfig::EmbeddingIndexingType::Code
           embedding_indexing_type_in,
+      EmbeddingIndexingConfig::QuantizationType::Code quantization_type_in,
       std::string&& path_in)
       : path(std::move(path_in)),
         id(id_in),
@@ -114,7 +124,8 @@
         tokenizer(tokenizer),
         term_match_type(term_match_type_in),
         numeric_match_type(numeric_match_type_in),
-        embedding_indexing_type(embedding_indexing_type_in) {}
+        embedding_indexing_type(embedding_indexing_type_in),
+        quantization_type(quantization_type_in) {}
 
   SectionMetadata(const SectionMetadata& other) = default;
   SectionMetadata& operator=(const SectionMetadata& other) = default;
@@ -126,7 +137,9 @@
     return path == rhs.path && id == rhs.id && data_type == rhs.data_type &&
            tokenizer == rhs.tokenizer &&
            term_match_type == rhs.term_match_type &&
-           numeric_match_type == rhs.numeric_match_type;
+           numeric_match_type == rhs.numeric_match_type &&
+           embedding_indexing_type == rhs.embedding_indexing_type &&
+           quantization_type == rhs.quantization_type;
   }
 };
 
diff --git a/icing/scoring/advanced_scoring/advanced-scorer.cc b/icing/scoring/advanced_scoring/advanced-scorer.cc
index 68b6971..d2029c8 100644
--- a/icing/scoring/advanced_scoring/advanced-scorer.cc
+++ b/icing/scoring/advanced_scoring/advanced-scorer.cc
@@ -16,20 +16,19 @@
 
 #include <cstdint>
 #include <memory>
-#include <string_view>
+#include <string>
+#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/feature-flags.h"
 #include "icing/index/embed/embedding-query-results.h"
 #include "icing/join/join-children-fetcher.h"
-#include "icing/query/advanced_query_parser/abstract-syntax-tree.h"
-#include "icing/query/advanced_query_parser/lexer.h"
-#include "icing/query/advanced_query_parser/parser.h"
+#include "icing/proto/scoring.pb.h"
 #include "icing/schema/schema-store.h"
+#include "icing/scoring/advanced_scoring/score-expression-util.h"
 #include "icing/scoring/advanced_scoring/score-expression.h"
-#include "icing/scoring/advanced_scoring/scoring-visitor.h"
 #include "icing/scoring/bm25f-calculator.h"
 #include "icing/scoring/section-weights.h"
 #include "icing/store/document-store.h"
@@ -40,35 +39,17 @@
 
 namespace {
 
-libtextclassifier3::StatusOr<std::unique_ptr<ScoreExpression>>
-GetScoreExpression(std::string_view scoring_expression, double default_score,
-                   SearchSpecProto::EmbeddingQueryMetricType::Code
-                       default_semantic_metric_type,
-                   const DocumentStore* document_store,
-                   const SchemaStore* schema_store, int64_t current_time_ms,
-                   const JoinChildrenFetcher* join_children_fetcher,
-                   const EmbeddingQueryResults* embedding_query_results,
-                   SectionWeights* section_weights,
-                   Bm25fCalculator* bm25f_calculator) {
-  Lexer lexer(scoring_expression, Lexer::Language::SCORING);
-  ICING_ASSIGN_OR_RETURN(std::vector<Lexer::LexerToken> lexer_tokens,
-                         std::move(lexer).ExtractTokens());
-  Parser parser = Parser::Create(std::move(lexer_tokens));
-  ICING_ASSIGN_OR_RETURN(std::unique_ptr<Node> tree_root,
-                         parser.ConsumeScoring());
-  ScoringVisitor visitor(default_score, default_semantic_metric_type,
-                         document_store, schema_store, section_weights,
-                         bm25f_calculator, join_children_fetcher,
-                         embedding_query_results, current_time_ms);
-  tree_root->Accept(&visitor);
-
-  ICING_ASSIGN_OR_RETURN(std::unique_ptr<ScoreExpression> expression,
-                         std::move(visitor).Expression());
-  if (expression->type() != ScoreExpressionType::kDouble) {
-    return absl_ports::InvalidArgumentError(
-        "The root scoring expression is not of double type.");
+std::unique_ptr<SchemaTypeAliasMap> BuildSchemaTypeAliasMap(
+    const ScoringSpecProto& scoring_spec) {
+  std::unique_ptr<SchemaTypeAliasMap> schema_type_alias_map =
+      std::make_unique<SchemaTypeAliasMap>();
+  for (const SchemaTypeAliasMapProto& alias_map_proto :
+       scoring_spec.schema_type_alias_map_protos()) {
+    (*schema_type_alias_map)[alias_map_proto.alias_schema_type()] =
+        std::unordered_set<std::string>(alias_map_proto.schema_types().begin(),
+                                        alias_map_proto.schema_types().end());
   }
-  return expression;
+  return schema_type_alias_map;
 }
 
 }  // namespace
@@ -81,24 +62,33 @@
                        const DocumentStore* document_store,
                        const SchemaStore* schema_store, int64_t current_time_ms,
                        const JoinChildrenFetcher* join_children_fetcher,
-                       const EmbeddingQueryResults* embedding_query_results) {
+                       const EmbeddingQueryResults* embedding_query_results,
+                       const FeatureFlags* feature_flags) {
   ICING_RETURN_ERROR_IF_NULL(document_store);
   ICING_RETURN_ERROR_IF_NULL(schema_store);
   ICING_RETURN_ERROR_IF_NULL(embedding_query_results);
+  ICING_RETURN_ERROR_IF_NULL(feature_flags);
 
   ICING_ASSIGN_OR_RETURN(std::unique_ptr<SectionWeights> section_weights,
                          SectionWeights::Create(schema_store, scoring_spec));
   std::unique_ptr<Bm25fCalculator> bm25f_calculator =
       std::make_unique<Bm25fCalculator>(document_store, section_weights.get(),
                                         current_time_ms);
+  std::unique_ptr<SchemaTypeAliasMap> schema_type_alias_map =
+      BuildSchemaTypeAliasMap(scoring_spec);
+  std::unique_ptr<std::unordered_set<ScoringFeatureType>>
+      scoring_feature_types_enabled =
+          score_expression_util::GetEnabledScoringFeatureTypes(scoring_spec);
 
   ICING_ASSIGN_OR_RETURN(
       std::unique_ptr<ScoreExpression> score_expression,
-      GetScoreExpression(scoring_spec.advanced_scoring_expression(),
-                         default_score, default_semantic_metric_type,
-                         document_store, schema_store, current_time_ms,
-                         join_children_fetcher, embedding_query_results,
-                         section_weights.get(), bm25f_calculator.get()));
+      score_expression_util::GetScoreExpression(
+          scoring_spec.advanced_scoring_expression(), default_score,
+          default_semantic_metric_type, document_store, schema_store,
+          current_time_ms, join_children_fetcher, embedding_query_results,
+          section_weights.get(), bm25f_calculator.get(),
+          schema_type_alias_map.get(), feature_flags,
+          scoring_feature_types_enabled.get()));
 
   std::vector<std::unique_ptr<ScoreExpression>> additional_score_expressions;
   additional_score_expressions.reserve(
@@ -107,17 +97,22 @@
        scoring_spec.additional_advanced_scoring_expressions()) {
     ICING_ASSIGN_OR_RETURN(
         std::unique_ptr<ScoreExpression> additional_score_expression,
-        GetScoreExpression(additional_expression, default_score,
-                           default_semantic_metric_type, document_store,
-                           schema_store, current_time_ms, join_children_fetcher,
-                           embedding_query_results, section_weights.get(),
-                           bm25f_calculator.get()));
+        score_expression_util::GetScoreExpression(
+            additional_expression, default_score, default_semantic_metric_type,
+            document_store, schema_store, current_time_ms,
+            join_children_fetcher, embedding_query_results,
+            section_weights.get(), bm25f_calculator.get(),
+            schema_type_alias_map.get(), feature_flags,
+            scoring_feature_types_enabled.get()));
+
     additional_score_expressions.push_back(
         std::move(additional_score_expression));
   }
   return std::unique_ptr<AdvancedScorer>(new AdvancedScorer(
       std::move(score_expression), std::move(additional_score_expressions),
-      std::move(section_weights), std::move(bm25f_calculator), default_score));
+      std::move(section_weights), std::move(bm25f_calculator),
+      std::move(schema_type_alias_map),
+      std::move(scoring_feature_types_enabled), default_score));
 }
 
 }  // namespace lib
diff --git a/icing/scoring/advanced_scoring/advanced-scorer.h b/icing/scoring/advanced_scoring/advanced-scorer.h
index 5fc5c48..eeb39c4 100644
--- a/icing/scoring/advanced_scoring/advanced-scorer.h
+++ b/icing/scoring/advanced_scoring/advanced-scorer.h
@@ -19,15 +19,18 @@
 #include <memory>
 #include <string>
 #include <unordered_map>
+#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/feature-flags.h"
 #include "icing/index/embed/embedding-query-results.h"
 #include "icing/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
 #include "icing/join/join-children-fetcher.h"
+#include "icing/proto/scoring.pb.h"
 #include "icing/schema/schema-store.h"
 #include "icing/scoring/advanced_scoring/score-expression.h"
 #include "icing/scoring/bm25f-calculator.h"
@@ -51,7 +54,8 @@
           default_semantic_metric_type,
       const DocumentStore* document_store, const SchemaStore* schema_store,
       int64_t current_time_ms, const JoinChildrenFetcher* join_children_fetcher,
-      const EmbeddingQueryResults* embedding_query_results);
+      const EmbeddingQueryResults* embedding_query_results,
+      const FeatureFlags* feature_flags);
 
   double GetScore(const DocHitInfo& hit_info,
                   const DocHitInfoIterator* query_it) override {
@@ -82,16 +86,23 @@
   bool is_constant() const { return score_expression_->is_constant(); }
 
  private:
-  explicit AdvancedScorer(std::unique_ptr<ScoreExpression> score_expression,
-                          std::vector<std::unique_ptr<ScoreExpression>>
-                              additional_score_expressions,
-                          std::unique_ptr<SectionWeights> section_weights,
-                          std::unique_ptr<Bm25fCalculator> bm25f_calculator,
-                          double default_score)
+  explicit AdvancedScorer(
+      std::unique_ptr<ScoreExpression> score_expression,
+      std::vector<std::unique_ptr<ScoreExpression>>
+          additional_score_expressions,
+      std::unique_ptr<SectionWeights> section_weights,
+      std::unique_ptr<Bm25fCalculator> bm25f_calculator,
+      std::unique_ptr<SchemaTypeAliasMap> alias_schema_type_map,
+      std::unique_ptr<std::unordered_set<ScoringFeatureType>>
+          scoring_feature_types_enabled,
+      double default_score)
       : score_expression_(std::move(score_expression)),
         additional_score_expressions_(std::move(additional_score_expressions)),
         section_weights_(std::move(section_weights)),
         bm25f_calculator_(std::move(bm25f_calculator)),
+        alias_schema_type_map_(std::move(alias_schema_type_map)),
+        scoring_feature_types_enabled_(
+            std::move(scoring_feature_types_enabled)),
         default_score_(default_score) {
     if (is_constant()) {
       ICING_LOG(WARNING)
@@ -118,6 +129,9 @@
   std::vector<std::unique_ptr<ScoreExpression>> additional_score_expressions_;
   std::unique_ptr<SectionWeights> section_weights_;
   std::unique_ptr<Bm25fCalculator> bm25f_calculator_;
+  std::unique_ptr<SchemaTypeAliasMap> alias_schema_type_map_;
+  std::unique_ptr<std::unordered_set<ScoringFeatureType>>
+      scoring_feature_types_enabled_;
   double default_score_;
 };
 
diff --git a/icing/scoring/advanced_scoring/advanced-scorer_fuzz_test.cc b/icing/scoring/advanced_scoring/advanced-scorer_fuzz_test.cc
index 9de9e8a..765ccdb 100644
--- a/icing/scoring/advanced_scoring/advanced-scorer_fuzz_test.cc
+++ b/icing/scoring/advanced_scoring/advanced-scorer_fuzz_test.cc
@@ -18,6 +18,7 @@
 #include <string>
 #include <string_view>
 
+#include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/index/embed/embedding-query-results.h"
@@ -25,12 +26,14 @@
 #include "icing/scoring/advanced_scoring/advanced-scorer.h"
 #include "icing/store/document-store.h"
 #include "icing/testing/fake-clock.h"
+#include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 
 namespace icing {
 namespace lib {
 
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   FakeClock fake_clock;
   Filesystem filesystem;
   const std::string test_dir = GetTestTempDir() + "/icing";
@@ -42,15 +45,16 @@
   filesystem.CreateDirectoryRecursively(schema_store_dir.c_str());
 
   std::unique_ptr<SchemaStore> schema_store =
-      SchemaStore::Create(&filesystem, schema_store_dir, &fake_clock)
+      SchemaStore::Create(&filesystem, schema_store_dir, &fake_clock,
+                          &feature_flags)
           .ValueOrDie();
   std::unique_ptr<DocumentStore> document_store =
       DocumentStore::Create(
           &filesystem, doc_store_dir, &fake_clock, schema_store.get(),
+          &feature_flags,
           /*force_recovery_and_revalidate_documents=*/false,
-          /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-          /*use_persistent_hash_map=*/true,
-          PortableFileBackedProtoLog<DocumentWrapper>::kDeflateCompressionLevel,
+          /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
+          PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
           /*initialize_stats=*/nullptr)
           .ValueOrDie()
           .document_store;
@@ -67,7 +71,7 @@
                          document_store.get(), schema_store.get(),
                          fake_clock.GetSystemTimeMilliseconds(),
                          /*join_children_fetcher=*/nullptr,
-                         &empty_embedding_query_results_);
+                         &empty_embedding_query_results_, &feature_flags);
 
   // Not able to test the GetScore method of AdvancedScorer, since it will only
   // be available after AdvancedScorer is successfully created. However, the
diff --git a/icing/scoring/advanced_scoring/advanced-scorer_test.cc b/icing/scoring/advanced_scoring/advanced-scorer_test.cc
index 8841d2c..ccd10c6 100644
--- a/icing/scoring/advanced_scoring/advanced-scorer_test.cc
+++ b/icing/scoring/advanced_scoring/advanced-scorer_test.cc
@@ -14,6 +14,7 @@
 
 #include "icing/scoring/advanced_scoring/advanced-scorer.h"
 
+#include <algorithm>
 #include <cmath>
 #include <cstdint>
 #include <memory>
@@ -28,11 +29,12 @@
 #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/index/embed/embedding-query-results.h"
 #include "icing/index/hit/doc-hit-info.h"
-#include "icing/join/join-children-fetcher.h"
+#include "icing/join/join-children-fetcher-impl-deprecated.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/proto/scoring.pb.h"
@@ -47,6 +49,7 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
+#include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 
 namespace icing {
@@ -66,28 +69,29 @@
         schema_store_dir_(test_dir_ + "/schema_store") {}
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
     filesystem_.CreateDirectoryRecursively(doc_store_dir_.c_str());
     filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str());
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(
-            &filesystem_, doc_store_dir_, &fake_clock_, schema_store_.get(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
 
-    // Creates a simple email schema
-    SchemaProto test_email_schema =
+    // Creates the schema
+    SchemaProto test_schema =
         SchemaBuilder()
             .AddType(SchemaTypeConfigBuilder().SetType("email").AddProperty(
                 PropertyConfigBuilder()
@@ -96,34 +100,74 @@
                         TermMatchType::PREFIX,
                         StringIndexingConfig::TokenizerType::PLAIN)
                     .SetCardinality(CARDINALITY_OPTIONAL)))
-            .AddType(SchemaTypeConfigBuilder()
-                         .SetType("person")
-                         .AddProperty(
-                             PropertyConfigBuilder()
-                                 .SetName("emailAddress")
-                                 .SetDataTypeString(
-                                     TermMatchType::PREFIX,
-                                     StringIndexingConfig::TokenizerType::PLAIN)
-                                 .SetCardinality(CARDINALITY_OPTIONAL))
-                         .AddProperty(
-                             PropertyConfigBuilder()
-                                 .SetName("name")
-                                 .SetDataTypeString(
-                                     TermMatchType::PREFIX,
-                                     StringIndexingConfig::TokenizerType::PLAIN)
-                                 .SetCardinality(CARDINALITY_OPTIONAL))
-
-                         .AddProperty(
-                             PropertyConfigBuilder()
-                                 .SetName("phoneNumber")
-                                 .SetDataTypeString(
-                                     TermMatchType::PREFIX,
-                                     StringIndexingConfig::TokenizerType::PLAIN)
-                                 .SetCardinality(CARDINALITY_OPTIONAL)))
+            .AddType(
+                SchemaTypeConfigBuilder()
+                    .SetType("message")
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("frequencyScore")
+                            .SetDataType(PropertyConfigProto::DataType::INT64)
+                            .SetCardinality(CARDINALITY_REPEATED)
+                            .SetScorableType(SCORABLE_TYPE_ENABLED))
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("content")
+                            .SetDataTypeString(
+                                TermMatchType::PREFIX,
+                                StringIndexingConfig::TokenizerType::PLAIN)
+                            .SetCardinality(CARDINALITY_OPTIONAL)))
+            .AddType(
+                SchemaTypeConfigBuilder()
+                    .SetType("person")
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("emailAddress")
+                            .SetDataTypeString(
+                                TermMatchType::PREFIX,
+                                StringIndexingConfig::TokenizerType::PLAIN)
+                            .SetCardinality(CARDINALITY_OPTIONAL))
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("name")
+                            .SetDataTypeString(
+                                TermMatchType::PREFIX,
+                                StringIndexingConfig::TokenizerType::PLAIN)
+                            .SetCardinality(CARDINALITY_OPTIONAL))
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("frequencyScore")
+                            .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                            .SetCardinality(CARDINALITY_REPEATED)
+                            .SetScorableType(SCORABLE_TYPE_ENABLED))
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("customizedScore")
+                            .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                            .SetCardinality(CARDINALITY_OPTIONAL)
+                            .SetScorableType(SCORABLE_TYPE_ENABLED))
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("contactTimes")
+                            .SetDataType(PropertyConfigProto::DataType::INT64)
+                            .SetCardinality(CARDINALITY_REPEATED)
+                            .SetScorableType(SCORABLE_TYPE_ENABLED))
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("isStarred")
+                            .SetDataType(PropertyConfigProto::DataType::BOOLEAN)
+                            .SetCardinality(CARDINALITY_OPTIONAL)
+                            .SetScorableType(SCORABLE_TYPE_ENABLED))
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("phoneNumber")
+                            .SetDataTypeString(
+                                TermMatchType::PREFIX,
+                                StringIndexingConfig::TokenizerType::PLAIN)
+                            .SetCardinality(CARDINALITY_OPTIONAL)))
             .Build();
 
     ICING_ASSERT_OK(schema_store_->SetSchema(
-        test_email_schema, /*ignore_errors_and_delete_documents=*/false,
+        test_schema, /*ignore_errors_and_delete_documents=*/false,
         /*allow_circular_schema_definitions=*/false));
   }
 
@@ -133,6 +177,7 @@
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   const std::string test_dir_;
   const std::string doc_store_dir_;
   const std::string schema_store_dir_;
@@ -182,6 +227,16 @@
   return scoring_spec;
 }
 
+void AddSchemaTypeAliasMap(ScoringSpecProto* scoring_spec,
+                           const std::string& alias_schema_type,
+                           const std::vector<std::string>& schema_types) {
+  SchemaTypeAliasMapProto* alias_map_proto =
+      scoring_spec->add_schema_type_alias_map_protos();
+  alias_map_proto->set_alias_schema_type(alias_schema_type);
+  alias_map_proto->mutable_schema_types()->Add(schema_types.begin(),
+                                               schema_types.end());
+}
+
 PropertyWeight CreatePropertyWeight(std::string path, double weight) {
   PropertyWeight property_weight;
   property_weight.set_path(std::move(path));
@@ -208,24 +263,24 @@
   ScoringSpecProto scoring_spec;
   scoring_spec.set_rank_by(
       ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION);
-  EXPECT_THAT(scorer_factory::Create(scoring_spec, /*default_score=*/10,
-                                     kDefaultSemanticMetricType,
-                                     document_store_.get(), schema_store_.get(),
-                                     fake_clock_.GetSystemTimeMilliseconds(),
-                                     /*join_children_fetcher=*/nullptr,
-                                     &empty_embedding_query_results_),
+  EXPECT_THAT(scorer_factory::Create(
+                  scoring_spec, /*default_score=*/10,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
   // Non-empty scoring expression for normal scoring
   scoring_spec = ScoringSpecProto::default_instance();
   scoring_spec.set_rank_by(ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE);
   scoring_spec.set_advanced_scoring_expression("1");
-  EXPECT_THAT(scorer_factory::Create(scoring_spec, /*default_score=*/10,
-                                     kDefaultSemanticMetricType,
-                                     document_store_.get(), schema_store_.get(),
-                                     fake_clock_.GetSystemTimeMilliseconds(),
-                                     /*join_children_fetcher=*/nullptr,
-                                     &empty_embedding_query_results_),
+  EXPECT_THAT(scorer_factory::Create(
+                  scoring_spec, /*default_score=*/10,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -242,7 +297,8 @@
                              document_store_.get(), schema_store_.get(),
                              fake_clock_.GetSystemTimeMilliseconds(),
                              /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
 
   DocHitInfo docHitInfo = DocHitInfo(document_id);
 
@@ -263,57 +319,58 @@
                              document_store_.get(), schema_store_.get(),
                              fake_clock_.GetSystemTimeMilliseconds(),
                              /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(3));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(CreateAdvancedScoringSpec("-1 + 2"),
-                             /*default_score=*/10, kDefaultSemanticMetricType,
-                             document_store_.get(), schema_store_.get(),
-                             fake_clock_.GetSystemTimeMilliseconds(),
-                             /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("-1 + 2"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(1));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(CreateAdvancedScoringSpec("1 + -2"),
-                             /*default_score=*/10, kDefaultSemanticMetricType,
-                             document_store_.get(), schema_store_.get(),
-                             fake_clock_.GetSystemTimeMilliseconds(),
-                             /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("1 + -2"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(-1));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(CreateAdvancedScoringSpec("1 - 2"),
-                             /*default_score=*/10, kDefaultSemanticMetricType,
-                             document_store_.get(), schema_store_.get(),
-                             fake_clock_.GetSystemTimeMilliseconds(),
-                             /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("1 - 2"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(-1));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(CreateAdvancedScoringSpec("1 * 2"),
-                             /*default_score=*/10, kDefaultSemanticMetricType,
-                             document_store_.get(), schema_store_.get(),
-                             fake_clock_.GetSystemTimeMilliseconds(),
-                             /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("1 * 2"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(2));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(CreateAdvancedScoringSpec("1 / 2"),
-                             /*default_score=*/10, kDefaultSemanticMetricType,
-                             document_store_.get(), schema_store_.get(),
-                             fake_clock_.GetSystemTimeMilliseconds(),
-                             /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("1 / 2"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(0.5));
 }
 
@@ -331,127 +388,128 @@
                              document_store_.get(), schema_store_.get(),
                              fake_clock_.GetSystemTimeMilliseconds(),
                              /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), DoubleNear(3, kEps));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("log(2.718281828459045)"),
-          /*default_score=*/10, kDefaultSemanticMetricType,
-          document_store_.get(), schema_store_.get(),
-          fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("log(2.718281828459045)"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), DoubleNear(1, kEps));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(CreateAdvancedScoringSpec("pow(2, 10)"),
-                             /*default_score=*/10, kDefaultSemanticMetricType,
-                             document_store_.get(), schema_store_.get(),
-                             fake_clock_.GetSystemTimeMilliseconds(),
-                             /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("pow(2, 10)"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(1024));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("max(10, 11, 12, 13, 14)"),
-          /*default_score=*/10, kDefaultSemanticMetricType,
-          document_store_.get(), schema_store_.get(),
-          fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("max(10, 11, 12, 13, 14)"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(14));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("min(10, 11, 12, 13, 14)"),
-          /*default_score=*/10, kDefaultSemanticMetricType,
-          document_store_.get(), schema_store_.get(),
-          fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("min(10, 11, 12, 13, 14)"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(10));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("len(10, 11, 12, 13, 14)"),
-          /*default_score=*/10, kDefaultSemanticMetricType,
-          document_store_.get(), schema_store_.get(),
-          fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("len(10, 11, 12, 13, 14)"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(5));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("sum(10, 11, 12, 13, 14)"),
-          /*default_score=*/10, kDefaultSemanticMetricType,
-          document_store_.get(), schema_store_.get(),
-          fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("sum(10, 11, 12, 13, 14)"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(10 + 11 + 12 + 13 + 14));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("avg(10, 11, 12, 13, 14)"),
-          /*default_score=*/10, kDefaultSemanticMetricType,
-          document_store_.get(), schema_store_.get(),
-          fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("avg(10, 11, 12, 13, 14)"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq((10 + 11 + 12 + 13 + 14) / 5.));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(CreateAdvancedScoringSpec("sqrt(2)"),
-                             /*default_score=*/10, kDefaultSemanticMetricType,
-                             document_store_.get(), schema_store_.get(),
-                             fake_clock_.GetSystemTimeMilliseconds(),
-                             /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("sqrt(2)"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), DoubleNear(sqrt(2), kEps));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(CreateAdvancedScoringSpec("abs(-2) + abs(2)"),
-                             /*default_score=*/10, kDefaultSemanticMetricType,
-                             document_store_.get(), schema_store_.get(),
-                             fake_clock_.GetSystemTimeMilliseconds(),
-                             /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("abs(-2) + abs(2)"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(4));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("sin(3.141592653589793)"),
-          /*default_score=*/10, kDefaultSemanticMetricType,
-          document_store_.get(), schema_store_.get(),
-          fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("sin(3.141592653589793)"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), DoubleNear(0, kEps));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("cos(3.141592653589793)"),
-          /*default_score=*/10, kDefaultSemanticMetricType,
-          document_store_.get(), schema_store_.get(),
-          fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("cos(3.141592653589793)"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), DoubleNear(-1, kEps));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("tan(3.141592653589793 / 4)"),
-          /*default_score=*/10, kDefaultSemanticMetricType,
-          document_store_.get(), schema_store_.get(),
-          fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("tan(3.141592653589793 / 4)"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), DoubleNear(1, kEps));
 }
 
@@ -471,28 +529,29 @@
                              document_store_.get(), schema_store_.get(),
                              fake_clock_.GetSystemTimeMilliseconds(),
                              /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(123));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("this.creationTimestamp()"),
-          /*default_score=*/10, kDefaultSemanticMetricType,
-          document_store_.get(), schema_store_.get(),
-          fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("this.creationTimestamp()"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(kDefaultCreationTimestampMs));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec(
-              "this.documentScore() + this.creationTimestamp()"),
-          /*default_score=*/10, kDefaultSemanticMetricType,
-          document_store_.get(), schema_store_.get(),
-          fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec(
+                      "this.documentScore() + this.creationTimestamp()"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo),
               Eq(123 + kDefaultCreationTimestampMs));
 }
@@ -512,7 +571,8 @@
           /*default_score=*/10, kDefaultSemanticMetricType,
           document_store_.get(), schema_store_.get(),
           fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(0));
   ICING_ASSERT_OK(document_store_->ReportUsage(
       CreateUsageReport("namespace", "uri", 100000, UsageReport::USAGE_TYPE1)));
@@ -525,31 +585,31 @@
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(300002));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("this.usageLastUsedTimestamp(1)"),
-          /*default_score=*/10, kDefaultSemanticMetricType,
-          document_store_.get(), schema_store_.get(),
-          fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("this.usageLastUsedTimestamp(1)"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(100000));
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("this.usageLastUsedTimestamp(2)"),
-          /*default_score=*/10, kDefaultSemanticMetricType,
-          document_store_.get(), schema_store_.get(),
-          fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("this.usageLastUsedTimestamp(2)"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(200000));
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("this.usageLastUsedTimestamp(3)"),
-          /*default_score=*/10, kDefaultSemanticMetricType,
-          document_store_.get(), schema_store_.get(),
-          fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("this.usageLastUsedTimestamp(3)"),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(300000));
 }
 
@@ -571,7 +631,8 @@
           CreateAdvancedScoringSpec("this.usageCount(4)"), default_score,
           kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(default_score));
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -580,7 +641,8 @@
           CreateAdvancedScoringSpec("this.usageCount(0)"), default_score,
           kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(default_score));
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -589,7 +651,8 @@
           CreateAdvancedScoringSpec("this.usageCount(1.5)"), default_score,
           kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(default_score));
 }
 
@@ -615,7 +678,8 @@
                              document_store_.get(), schema_store_.get(),
                              fake_clock_.GetSystemTimeMilliseconds(),
                              /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
   scorer->PrepareToScore(/*query_term_iterators=*/{});
 
   // Should get the default score.
@@ -660,7 +724,10 @@
   map_joinable_qualified_id[document_id_1].push_back(fake_child1);
   map_joinable_qualified_id[document_id_1].push_back(fake_child2);
   map_joinable_qualified_id[document_id_2].push_back(fake_child3);
-  JoinChildrenFetcher fetcher(join_spec, std::move(map_joinable_qualified_id));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<JoinChildrenFetcherImplDeprecated> fetcher,
+      JoinChildrenFetcherImplDeprecated::Create(
+          join_spec, std::move(map_joinable_qualified_id)));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<AdvancedScorer> scorer,
@@ -668,7 +735,8 @@
           CreateAdvancedScoringSpec("len(this.childrenRankingSignals())"),
           default_score, kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          &fetcher, &empty_embedding_query_results_));
+          fetcher.get(), &empty_embedding_query_results_,
+          feature_flags_.get()));
   // document_id_1 has two children.
   EXPECT_THAT(scorer->GetScore(docHitInfo1, /*query_it=*/nullptr), Eq(2));
   // document_id_2 has one child.
@@ -682,7 +750,8 @@
           CreateAdvancedScoringSpec("sum(this.childrenRankingSignals())"),
           default_score, kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          &fetcher, &empty_embedding_query_results_));
+          fetcher.get(), &empty_embedding_query_results_,
+          feature_flags_.get()));
   // document_id_1 has two children with scores 1 and 2.
   EXPECT_THAT(scorer->GetScore(docHitInfo1, /*query_it=*/nullptr), Eq(3));
   // document_id_2 has one child with score 4.
@@ -696,7 +765,8 @@
           CreateAdvancedScoringSpec("avg(this.childrenRankingSignals())"),
           default_score, kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          &fetcher, &empty_embedding_query_results_));
+          fetcher.get(), &empty_embedding_query_results_,
+          feature_flags_.get()));
   // document_id_1 has two children with scores 1 and 2.
   EXPECT_THAT(scorer->GetScore(docHitInfo1, /*query_it=*/nullptr), Eq(3 / 2.));
   // document_id_2 has one child with score 4.
@@ -707,15 +777,15 @@
               Eq(default_score));
 
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec(
-              // Equivalent to "avg(this.childrenRankingSignals())"
-              "sum(this.childrenRankingSignals()) / "
-              "len(this.childrenRankingSignals())"),
-          default_score, kDefaultSemanticMetricType, document_store_.get(),
-          schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          &fetcher, &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec(
+                      // Equivalent to "avg(this.childrenRankingSignals())"
+                      "sum(this.childrenRankingSignals()) / "
+                      "len(this.childrenRankingSignals())"),
+                  default_score, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(), fetcher.get(),
+                  &empty_embedding_query_results_, feature_flags_.get()));
   // document_id_1 has two children with scores 1 and 2.
   EXPECT_THAT(scorer->GetScore(docHitInfo1, /*query_it=*/nullptr), Eq(3 / 2.));
   // document_id_2 has one child with score 4.
@@ -782,7 +852,8 @@
                              document_store_.get(), schema_store_.get(),
                              fake_clock_.GetSystemTimeMilliseconds(),
                              /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
   // min([1]) = 1
   EXPECT_THAT(scorer->GetScore(doc_hit_info_1, /*query_it=*/nullptr), Eq(1));
   // min([0.5, 0.8]) = 0.5
@@ -792,13 +863,13 @@
 
   spec_proto.set_advanced_scoring_expression("max(this.propertyWeights())");
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(spec_proto,
-                             /*default_score=*/10, kDefaultSemanticMetricType,
-                             document_store_.get(), schema_store_.get(),
-                             fake_clock_.GetSystemTimeMilliseconds(),
-                             /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  spec_proto,
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   // max([1]) = 1
   EXPECT_THAT(scorer->GetScore(doc_hit_info_1, /*query_it=*/nullptr), Eq(1));
   // max([0.5, 0.8]) = 0.8
@@ -808,13 +879,13 @@
 
   spec_proto.set_advanced_scoring_expression("sum(this.propertyWeights())");
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(spec_proto,
-                             /*default_score=*/10, kDefaultSemanticMetricType,
-                             document_store_.get(), schema_store_.get(),
-                             fake_clock_.GetSystemTimeMilliseconds(),
-                             /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  spec_proto,
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   // sum([1]) = 1
   EXPECT_THAT(scorer->GetScore(doc_hit_info_1, /*query_it=*/nullptr), Eq(1));
   // sum([0.5, 0.8]) = 1.3
@@ -869,7 +940,8 @@
                              document_store_.get(), schema_store_.get(),
                              fake_clock_.GetSystemTimeMilliseconds(),
                              /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
   // min([1]) = 1
   EXPECT_THAT(scorer->GetScore(doc_hit_info_1, /*query_it=*/nullptr), Eq(1));
   // min([0.5, 1, 0.5]) = 0.5
@@ -877,13 +949,13 @@
 
   spec_proto.set_advanced_scoring_expression("max(this.propertyWeights())");
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(spec_proto,
-                             /*default_score=*/10, kDefaultSemanticMetricType,
-                             document_store_.get(), schema_store_.get(),
-                             fake_clock_.GetSystemTimeMilliseconds(),
-                             /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  spec_proto,
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   // max([1]) = 1
   EXPECT_THAT(scorer->GetScore(doc_hit_info_1, /*query_it=*/nullptr), Eq(1));
   // max([0.5, 1, 0.5]) = 1
@@ -891,13 +963,13 @@
 
   spec_proto.set_advanced_scoring_expression("sum(this.propertyWeights())");
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer,
-      AdvancedScorer::Create(spec_proto,
-                             /*default_score=*/10, kDefaultSemanticMetricType,
-                             document_store_.get(), schema_store_.get(),
-                             fake_clock_.GetSystemTimeMilliseconds(),
-                             /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+      scorer, AdvancedScorer::Create(
+                  spec_proto,
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
   // sum([1]) = 1
   EXPECT_THAT(scorer->GetScore(doc_hit_info_1, /*query_it=*/nullptr), Eq(1));
   // sum([0.5, 1, 0.5]) = 2
@@ -914,19 +986,26 @@
           CreateAdvancedScoringSpec("len(this.childrenRankingSignals())"),
           default_score, kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
   // The root expression can only be of double type, but here it is of list
   // type.
-  JoinChildrenFetcher fake_fetcher(JoinSpecProto::default_instance(),
-                                   /*map_joinable_qualified_id=*/{});
+  JoinSpecProto join_spec;
+  join_spec.set_parent_property_expression("this.qualifiedId()");
+  join_spec.set_child_property_expression("sender");
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<JoinChildrenFetcherImplDeprecated> fetcher,
+      JoinChildrenFetcherImplDeprecated::Create(
+          join_spec,
+          /*map_joinable_qualified_id=*/{}));
   EXPECT_THAT(
       AdvancedScorer::Create(
           CreateAdvancedScoringSpec("this.childrenRankingSignals()"),
           default_score, kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          &fake_fetcher, &empty_embedding_query_results_),
+          fetcher.get(), &empty_embedding_query_results_, feature_flags_.get()),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -954,7 +1033,8 @@
                              document_store_.get(), schema_store_.get(),
                              fake_clock_.GetSystemTimeMilliseconds(),
                              /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
   EXPECT_FALSE(scorer->is_constant());
   scorer->PrepareToScore(/*query_term_iterators=*/{});
 
@@ -982,29 +1062,32 @@
                              document_store_.get(), schema_store_.get(),
                              fake_clock_.GetSystemTimeMilliseconds(),
                              /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
   EXPECT_TRUE(scorer->is_constant());
 }
 
 // Should be a parsing Error
 TEST_F(AdvancedScorerTest, EmptyExpression) {
-  EXPECT_THAT(
-      AdvancedScorer::Create(CreateAdvancedScoringSpec(""),
-                             /*default_score=*/10, kDefaultSemanticMetricType,
-                             document_store_.get(), schema_store_.get(),
-                             fake_clock_.GetSystemTimeMilliseconds(),
-                             /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_),
-      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec(""),
+                  /*default_score=*/10, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
 TEST_F(AdvancedScorerTest, ConstantEvaluationErrorShouldReturnAnError) {
   libtextclassifier3::StatusOr<std::unique_ptr<AdvancedScorer>> scorer_or =
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("log(0)"), /*default_score=*/0,
-          kDefaultSemanticMetricType, document_store_.get(),
-          schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_);
+      AdvancedScorer::Create(CreateAdvancedScoringSpec("log(0)"),
+                             /*default_score=*/0, kDefaultSemanticMetricType,
+                             document_store_.get(), schema_store_.get(),
+                             fake_clock_.GetSystemTimeMilliseconds(),
+                             /*join_children_fetcher=*/nullptr,
+                             &empty_embedding_query_results_,
+                             feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1014,7 +1097,8 @@
       CreateAdvancedScoringSpec("1 / 0"), /*default_score=*/0,
       kDefaultSemanticMetricType, document_store_.get(), schema_store_.get(),
       fake_clock_.GetSystemTimeMilliseconds(),
-      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_);
+      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+      feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1024,7 +1108,8 @@
       CreateAdvancedScoringSpec("sqrt(-1)"), /*default_score=*/0,
       kDefaultSemanticMetricType, document_store_.get(), schema_store_.get(),
       fake_clock_.GetSystemTimeMilliseconds(),
-      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_);
+      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+      feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1034,7 +1119,8 @@
       CreateAdvancedScoringSpec("pow(-1, 0.5)"), /*default_score=*/0,
       kDefaultSemanticMetricType, document_store_.get(), schema_store_.get(),
       fake_clock_.GetSystemTimeMilliseconds(),
-      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_);
+      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+      feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1058,7 +1144,8 @@
           CreateAdvancedScoringSpec("log(this.documentScore())"), default_score,
           kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), DoubleNear(default_score, kEps));
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1067,7 +1154,8 @@
           CreateAdvancedScoringSpec("1 / this.documentScore()"), default_score,
           kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), DoubleNear(default_score, kEps));
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1076,7 +1164,8 @@
           CreateAdvancedScoringSpec("sqrt(this.documentScore() - 1)"),
           default_score, kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), DoubleNear(default_score, kEps));
 
   ICING_ASSERT_OK_AND_ASSIGN(
@@ -1085,7 +1174,8 @@
           CreateAdvancedScoringSpec("pow(this.documentScore() - 1, 0.5)"),
           default_score, kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(docHitInfo), DoubleNear(default_score, kEps));
 }
 
@@ -1094,176 +1184,181 @@
 TEST_F(AdvancedScorerTest, MathTypeError) {
   const double default_score = 0;
 
-  EXPECT_THAT(AdvancedScorer::Create(CreateAdvancedScoringSpec("test"),
-                                     default_score, kDefaultSemanticMetricType,
-                                     document_store_.get(), schema_store_.get(),
-                                     fake_clock_.GetSystemTimeMilliseconds(),
-                                     /*join_children_fetcher=*/nullptr,
-                                     &empty_embedding_query_results_),
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("test"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
-  EXPECT_THAT(AdvancedScorer::Create(CreateAdvancedScoringSpec("log()"),
-                                     default_score, kDefaultSemanticMetricType,
-                                     document_store_.get(), schema_store_.get(),
-                                     fake_clock_.GetSystemTimeMilliseconds(),
-                                     /*join_children_fetcher=*/nullptr,
-                                     &empty_embedding_query_results_),
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("log()"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
-  EXPECT_THAT(AdvancedScorer::Create(CreateAdvancedScoringSpec("log(1, 2, 3)"),
-                                     default_score, kDefaultSemanticMetricType,
-                                     document_store_.get(), schema_store_.get(),
-                                     fake_clock_.GetSystemTimeMilliseconds(),
-                                     /*join_children_fetcher=*/nullptr,
-                                     &empty_embedding_query_results_),
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("log(1, 2, 3)"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
-  EXPECT_THAT(AdvancedScorer::Create(CreateAdvancedScoringSpec("log(1, this)"),
-                                     default_score, kDefaultSemanticMetricType,
-                                     document_store_.get(), schema_store_.get(),
-                                     fake_clock_.GetSystemTimeMilliseconds(),
-                                     /*join_children_fetcher=*/nullptr,
-                                     &empty_embedding_query_results_),
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("log(1, this)"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
-  EXPECT_THAT(AdvancedScorer::Create(CreateAdvancedScoringSpec("pow(1)"),
-                                     default_score, kDefaultSemanticMetricType,
-                                     document_store_.get(), schema_store_.get(),
-                                     fake_clock_.GetSystemTimeMilliseconds(),
-                                     /*join_children_fetcher=*/nullptr,
-                                     &empty_embedding_query_results_),
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("pow(1)"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
-  EXPECT_THAT(AdvancedScorer::Create(CreateAdvancedScoringSpec("sqrt(1, 2)"),
-                                     default_score, kDefaultSemanticMetricType,
-                                     document_store_.get(), schema_store_.get(),
-                                     fake_clock_.GetSystemTimeMilliseconds(),
-                                     /*join_children_fetcher=*/nullptr,
-                                     &empty_embedding_query_results_),
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("sqrt(1, 2)"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
-  EXPECT_THAT(AdvancedScorer::Create(CreateAdvancedScoringSpec("abs(1, 2)"),
-                                     default_score, kDefaultSemanticMetricType,
-                                     document_store_.get(), schema_store_.get(),
-                                     fake_clock_.GetSystemTimeMilliseconds(),
-                                     /*join_children_fetcher=*/nullptr,
-                                     &empty_embedding_query_results_),
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("abs(1, 2)"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
-  EXPECT_THAT(AdvancedScorer::Create(CreateAdvancedScoringSpec("sin(1, 2)"),
-                                     default_score, kDefaultSemanticMetricType,
-                                     document_store_.get(), schema_store_.get(),
-                                     fake_clock_.GetSystemTimeMilliseconds(),
-                                     /*join_children_fetcher=*/nullptr,
-                                     &empty_embedding_query_results_),
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("sin(1, 2)"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
-  EXPECT_THAT(AdvancedScorer::Create(CreateAdvancedScoringSpec("cos(1, 2)"),
-                                     default_score, kDefaultSemanticMetricType,
-                                     document_store_.get(), schema_store_.get(),
-                                     fake_clock_.GetSystemTimeMilliseconds(),
-                                     /*join_children_fetcher=*/nullptr,
-                                     &empty_embedding_query_results_),
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("cos(1, 2)"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
-  EXPECT_THAT(AdvancedScorer::Create(CreateAdvancedScoringSpec("tan(1, 2)"),
-                                     default_score, kDefaultSemanticMetricType,
-                                     document_store_.get(), schema_store_.get(),
-                                     fake_clock_.GetSystemTimeMilliseconds(),
-                                     /*join_children_fetcher=*/nullptr,
-                                     &empty_embedding_query_results_),
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("tan(1, 2)"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
-  EXPECT_THAT(AdvancedScorer::Create(CreateAdvancedScoringSpec("this"),
-                                     default_score, kDefaultSemanticMetricType,
-                                     document_store_.get(), schema_store_.get(),
-                                     fake_clock_.GetSystemTimeMilliseconds(),
-                                     /*join_children_fetcher=*/nullptr,
-                                     &empty_embedding_query_results_),
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("this"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
-  EXPECT_THAT(AdvancedScorer::Create(CreateAdvancedScoringSpec("-this"),
-                                     default_score, kDefaultSemanticMetricType,
-                                     document_store_.get(), schema_store_.get(),
-                                     fake_clock_.GetSystemTimeMilliseconds(),
-                                     /*join_children_fetcher=*/nullptr,
-                                     &empty_embedding_query_results_),
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("-this"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 
-  EXPECT_THAT(AdvancedScorer::Create(CreateAdvancedScoringSpec("1 + this"),
-                                     default_score, kDefaultSemanticMetricType,
-                                     document_store_.get(), schema_store_.get(),
-                                     fake_clock_.GetSystemTimeMilliseconds(),
-                                     /*join_children_fetcher=*/nullptr,
-                                     &empty_embedding_query_results_),
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("1 + this"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
 TEST_F(AdvancedScorerTest, DocumentFunctionTypeError) {
   const double default_score = 0;
 
-  EXPECT_THAT(
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("documentScore(1)"), default_score,
-          kDefaultSemanticMetricType, document_store_.get(),
-          schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_),
-      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("documentScore(1)"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(
       AdvancedScorer::Create(
           CreateAdvancedScoringSpec("this.creationTimestamp(1)"), default_score,
           kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
-  EXPECT_THAT(
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("this.usageCount()"), default_score,
-          kDefaultSemanticMetricType, document_store_.get(),
-          schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_),
-      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("this.usageCount()"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(
       AdvancedScorer::Create(
           CreateAdvancedScoringSpec("usageLastUsedTimestamp(1, 1)"),
           default_score, kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
-  EXPECT_THAT(
-      AdvancedScorer::Create(
-          CreateAdvancedScoringSpec("relevanceScore(1)"), default_score,
-          kDefaultSemanticMetricType, document_store_.get(),
-          schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_),
-      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("relevanceScore(1)"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(
       AdvancedScorer::Create(
           CreateAdvancedScoringSpec("documentScore(this)"), default_score,
           kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(
       AdvancedScorer::Create(
           CreateAdvancedScoringSpec("that.documentScore()"), default_score,
           kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(
       AdvancedScorer::Create(
           CreateAdvancedScoringSpec("this.this.creationTimestamp()"),
           default_score, kDefaultSemanticMetricType, document_store_.get(),
           schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()),
       StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
-  EXPECT_THAT(AdvancedScorer::Create(CreateAdvancedScoringSpec("this.log(2)"),
-                                     default_score, kDefaultSemanticMetricType,
-                                     document_store_.get(), schema_store_.get(),
-                                     fake_clock_.GetSystemTimeMilliseconds(),
-                                     /*join_children_fetcher=*/nullptr,
-                                     &empty_embedding_query_results_),
+  EXPECT_THAT(AdvancedScorer::Create(
+                  CreateAdvancedScoringSpec("this.log(2)"), default_score,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
 }
 
@@ -1283,7 +1378,8 @@
           kDefaultSemanticMetricType, kDefaultSemanticMetricType,
           document_store_.get(), schema_store_.get(),
           fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &embedding_query_results);
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1294,7 +1390,8 @@
       kDefaultSemanticMetricType, kDefaultSemanticMetricType,
       document_store_.get(), schema_store_.get(),
       fake_clock_.GetSystemTimeMilliseconds(),
-      /*join_children_fetcher=*/nullptr, &embedding_query_results);
+      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+      feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1306,7 +1403,8 @@
       kDefaultSemanticMetricType, kDefaultSemanticMetricType,
       document_store_.get(), schema_store_.get(),
       fake_clock_.GetSystemTimeMilliseconds(),
-      /*join_children_fetcher=*/nullptr, &embedding_query_results);
+      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+      feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1318,7 +1416,8 @@
       kDefaultSemanticMetricType, kDefaultSemanticMetricType,
       document_store_.get(), schema_store_.get(),
       fake_clock_.GetSystemTimeMilliseconds(),
-      /*join_children_fetcher=*/nullptr, &embedding_query_results);
+      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+      feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1330,7 +1429,8 @@
       kDefaultSemanticMetricType, kDefaultSemanticMetricType,
       document_store_.get(), schema_store_.get(),
       fake_clock_.GetSystemTimeMilliseconds(),
-      /*join_children_fetcher=*/nullptr, &embedding_query_results);
+      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+      feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1342,7 +1442,8 @@
       kDefaultSemanticMetricType, kDefaultSemanticMetricType,
       document_store_.get(), schema_store_.get(),
       fake_clock_.GetSystemTimeMilliseconds(),
-      /*join_children_fetcher=*/nullptr, &embedding_query_results);
+      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+      feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1354,7 +1455,8 @@
       kDefaultSemanticMetricType, kDefaultSemanticMetricType,
       document_store_.get(), schema_store_.get(),
       fake_clock_.GetSystemTimeMilliseconds(),
-      /*join_children_fetcher=*/nullptr, &embedding_query_results);
+      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+      feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1383,7 +1485,8 @@
                              document_store_.get(), schema_store_.get(),
                              fake_clock_.GetSystemTimeMilliseconds(),
                              /*join_children_fetcher=*/nullptr,
-                             &embedding_query_results);
+                             &empty_embedding_query_results_,
+                             feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1395,7 +1498,8 @@
                                 "getEmbeddingParameter(1), \"COSINE\"))"),
       /*default_score=*/0, kDefaultSemanticMetricType, document_store_.get(),
       schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-      /*join_children_fetcher=*/nullptr, &embedding_query_results);
+      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+      feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1407,7 +1511,8 @@
                                 "getEmbeddingParameter(2)))"),
       /*default_score=*/0, kDefaultSemanticMetricType, document_store_.get(),
       schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-      /*join_children_fetcher=*/nullptr, &embedding_query_results);
+      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+      feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1425,7 +1530,8 @@
           /*default_score=*/0, kDefaultSemanticMetricType,
           document_store_.get(), schema_store_.get(),
           fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_);
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1437,7 +1543,8 @@
                                 "getEmbeddingParameter(pow(2, 50))))"),
       /*default_score=*/0, kDefaultSemanticMetricType, document_store_.get(),
       schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_);
+      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+      feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1449,7 +1556,8 @@
                                 "getEmbeddingParameter(0.5)))"),
       /*default_score=*/0, kDefaultSemanticMetricType, document_store_.get(),
       schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
-      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_);
+      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+      feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1526,7 +1634,8 @@
           SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT,
           document_store_.get(), schema_store_.get(),
           fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &embedding_query_results,
+          feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(doc_hit_info_0), DoubleNear(0.5, kEps));
   EXPECT_THAT(scorer->GetScore(doc_hit_info_1), DoubleNear(0.6, kEps));
 
@@ -1541,7 +1650,8 @@
           SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT,
           document_store_.get(), schema_store_.get(),
           fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &embedding_query_results,
+          feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(doc_hit_info_0), DoubleNear(0.1 + 0.2, kEps));
   EXPECT_THAT(scorer->GetScore(doc_hit_info_1), DoubleNear(0.3 + 0.4, kEps));
 
@@ -1559,7 +1669,8 @@
                   SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT,
                   document_store_.get(), schema_store_.get(),
                   fake_clock_.GetSystemTimeMilliseconds(),
-                  /*join_children_fetcher=*/nullptr, &embedding_query_results));
+                  /*join_children_fetcher=*/nullptr, &embedding_query_results,
+                  feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(doc_hit_info_0),
               DoubleNear(0.1 + 0.2 + 0.5 + 0.7, kEps));
   EXPECT_THAT(scorer->GetScore(doc_hit_info_1),
@@ -1575,7 +1686,8 @@
           SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT,
           document_store_.get(), schema_store_.get(),
           fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &embedding_query_results));
+          /*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));
 
@@ -1588,7 +1700,8 @@
           SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT,
           document_store_.get(), schema_store_.get(),
           fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &embedding_query_results);
+          /*join_children_fetcher=*/nullptr, &embedding_query_results,
+          feature_flags_.get());
   EXPECT_THAT(scorer_or,
               StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
   EXPECT_THAT(scorer_or.status().error_message(),
@@ -1633,7 +1746,8 @@
           SearchSpecProto::EmbeddingQueryMetricType::COSINE,
           document_store_.get(), schema_store_.get(),
           fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &embedding_query_results,
+          feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(doc_hit_info_0), DoubleNear(5, kEps));
 
   // minOrDefault({4, 5, 2, 1, 3}, -100) = 1
@@ -1646,7 +1760,8 @@
           SearchSpecProto::EmbeddingQueryMetricType::COSINE,
           document_store_.get(), schema_store_.get(),
           fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &embedding_query_results,
+          feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(doc_hit_info_0), DoubleNear(1, kEps));
 
   // maxOrDefault({}, 100) = 100
@@ -1659,7 +1774,8 @@
           SearchSpecProto::EmbeddingQueryMetricType::COSINE,
           document_store_.get(), schema_store_.get(),
           fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &embedding_query_results,
+          feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(doc_hit_info_0), DoubleNear(100, kEps));
 
   // minOrDefault({}, -100) = -100
@@ -1672,20 +1788,22 @@
           SearchSpecProto::EmbeddingQueryMetricType::COSINE,
           document_store_.get(), schema_store_.get(),
           fake_clock_.GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &embedding_query_results,
+          feature_flags_.get()));
   EXPECT_THAT(scorer->GetScore(doc_hit_info_0), DoubleNear(-100, kEps));
 
   // sum(filterByRange({4, 5, 2, 1, 3}, 2, 4)) = sum({4, 2, 3}) = 9
   ICING_ASSERT_OK_AND_ASSIGN(
-      scorer, AdvancedScorer::Create(
-                  CreateAdvancedScoringSpec(
-                      "sum(filterByRange(this.matchedSemanticScores("
-                      "getEmbeddingParameter(0)), 2, 4))"),
-                  kDefaultScore, /*default_semantic_metric_type=*/
-                  SearchSpecProto::EmbeddingQueryMetricType::COSINE,
-                  document_store_.get(), schema_store_.get(),
-                  fake_clock_.GetSystemTimeMilliseconds(),
-                  /*join_children_fetcher=*/nullptr, &embedding_query_results));
+      scorer,
+      AdvancedScorer::Create(CreateAdvancedScoringSpec(
+                                 "sum(filterByRange(this.matchedSemanticScores("
+                                 "getEmbeddingParameter(0)), 2, 4))"),
+                             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(9, kEps));
 }
 
@@ -1714,7 +1832,8 @@
                              document_store_.get(), schema_store_.get(),
                              fake_clock_.GetSystemTimeMilliseconds(),
                              /*join_children_fetcher=*/nullptr,
-                             &empty_embedding_query_results_));
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
   EXPECT_FALSE(scorer->is_constant());
   scorer->PrepareToScore(/*query_term_iterators=*/{});
   EXPECT_THAT(scorer->GetScore(docHitInfo, /*query_it=*/nullptr),
@@ -1725,6 +1844,728 @@
               ElementsAre(DoubleNear(4, kEps), DoubleNear(127, kEps)));
 }
 
+TEST_F(AdvancedScorerTest, GetScorablePropertyWithFeatureDisabled) {
+  ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "sum(getScorableProperty(\"email\"))");
+
+  EXPECT_THAT(
+      AdvancedScorer::Create(
+          scoring_spec, /*default_score=*/10, kDefaultSemanticMetricType,
+          document_store_.get(), schema_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+               HasSubstr("SCORABLE_PROPERTY_RANKING feature is not enabled.")));
+}
+
+TEST_F(AdvancedScorerTest, GetScorableProperty_WrongParamsNumber) {
+  ScoringSpecProto scoring_spec_with_one_param = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "sum(getScorableProperty(\"email\"))");
+  scoring_spec_with_one_param.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+
+  EXPECT_THAT(
+      AdvancedScorer::Create(
+          scoring_spec_with_one_param, /*default_score=*/10,
+          kDefaultSemanticMetricType, document_store_.get(),
+          schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+               HasSubstr(
+                   "getScorableProperty must take exactly two string params")));
+
+  ScoringSpecProto scoring_spec_with_int_param = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "sum(getScorableProperty(\"email\", 123))");
+  scoring_spec_with_int_param.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+
+  EXPECT_THAT(
+      AdvancedScorer::Create(
+          scoring_spec_with_int_param, /*default_score=*/10,
+          kDefaultSemanticMetricType, document_store_.get(),
+          schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+               HasSubstr(
+                   "getScorableProperty must take exactly two string params")));
+}
+
+TEST_F(AdvancedScorerTest, GetScorableProperty_ParamsMustBeString) {
+  ScoringSpecProto scoring_spec_with_int_param = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "sum(getScorableProperty(\"email\", 123))");
+  scoring_spec_with_int_param.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+
+  EXPECT_THAT(
+      AdvancedScorer::Create(
+          scoring_spec_with_int_param, /*default_score=*/10,
+          kDefaultSemanticMetricType, document_store_.get(),
+          schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+               HasSubstr(
+                   "getScorableProperty must take exactly two string params")));
+}
+
+TEST_F(AdvancedScorerTest, GetScorableProperty_SchemaNotExistInSchemaStore) {
+  ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "sum(getScorableProperty(\"aliasEmail\", \"frequencyScore\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "aliasEmail", {"non_exist"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+
+  EXPECT_THAT(AdvancedScorer::Create(
+                  scoring_spec, /*default_score=*/10,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::OK));
+}
+
+TEST_F(AdvancedScorerTest, GetScorableProperty_PropertyNameNotScorable) {
+  ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "sum(getScorableProperty(\"aliasEmail\", \"subject\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "aliasEmail", {"email"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+
+  EXPECT_THAT(AdvancedScorer::Create(
+                  scoring_spec, /*default_score=*/10,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+                       HasSubstr("'subject' is not defined as a scorable "
+                                 "property under schema type")));
+}
+
+TEST_F(AdvancedScorerTest, GetScorableProperty_PropertyNameNotExist) {
+  ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "sum(getScorableProperty(\"aliasEmail\", \"non_exist\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "aliasEmail", {"email"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+
+  EXPECT_THAT(AdvancedScorer::Create(
+                  scoring_spec, /*default_score=*/10,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+                       HasSubstr("'non_exist' is not defined as a scorable "
+                                 "property under schema type")));
+}
+
+TEST_F(AdvancedScorerTest, GetScorableProperty_SomePropertiesNotScorable) {
+  ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "100 * avg(getScorableProperty(\"aliasPerson\", \"isStarred\")) + "
+      "10  * max(getScorableProperty(\"aliasPerson\", \"frequencyScore\")) + "
+      "10  * sum(getScorableProperty(\"aliasPerson\", \"non_exist\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "aliasPerson", {"person"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+
+  EXPECT_THAT(AdvancedScorer::Create(
+                  scoring_spec, /*default_score=*/10,
+                  kDefaultSemanticMetricType, document_store_.get(),
+                  schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+                       HasSubstr("'non_exist' is not defined as a scorable "
+                                 "property under schema type")));
+}
+
+TEST_F(AdvancedScorerTest,
+       GetScorableProperty_DocumentSchemaDifferentFromScoringSpecSchema) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace", "uri")
+                               .SetSchema("email")
+                               .SetScore(100)
+                               .SetCreationTimestampMs(123)
+                               .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             document_store_->Put(document));
+  DocHitInfo docHitInfo(put_result.new_document_id);
+
+  // getScorableProperty("aliasPerson", "frequencyScore") will return an empty
+  // list because the schema type of the document is "email" instead of
+  // "person".
+  ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "sum(getScorableProperty(\"aliasPerson\", \"frequencyScore\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "aliasPerson", {"person"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  double expected_score = 100 + 0;
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<AdvancedScorer> scorer,
+      AdvancedScorer::Create(
+          scoring_spec, /*default_score=*/10, kDefaultSemanticMetricType,
+          document_store_.get(), schema_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()));
+  scorer->PrepareToScore(/*query_term_iterators=*/{});
+  EXPECT_THAT(scorer->GetScore(docHitInfo, /*query_it=*/nullptr),
+              DoubleNear(expected_score, kEps));
+}
+
+TEST_F(AdvancedScorerTest,
+       GetScorableProperty_DocumentWithoutScorableProperties) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace", "uri")
+                               .SetSchema("person")
+                               .SetScore(100)
+                               .SetCreationTimestampMs(123)
+                               .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             document_store_->Put(document));
+  DocHitInfo docHitInfo(put_result.new_document_id);
+
+  // getScorableProperty("aliasPerson", "frequencyScore") will return an empty
+  // list.
+  ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "sum(getScorableProperty(\"aliasPerson\", \"frequencyScore\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "aliasPerson", {"person"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  double expected_score = 100 + 0;
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<AdvancedScorer> scorer,
+      AdvancedScorer::Create(
+          scoring_spec, /*default_score=*/10, kDefaultSemanticMetricType,
+          document_store_.get(), schema_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()));
+  scorer->PrepareToScore(/*query_term_iterators=*/{});
+  EXPECT_THAT(scorer->GetScore(docHitInfo, /*query_it=*/nullptr),
+              DoubleNear(expected_score, kEps));
+}
+
+TEST_F(AdvancedScorerTest, GetScorableProperty_WithDoubleList) {
+  DocumentProto document =
+      DocumentBuilder()
+          .SetKey("namespace", "uri")
+          .SetSchema("person")
+          .SetScore(100)
+          .SetCreationTimestampMs(123)
+          .AddDoubleProperty("frequencyScore", 1.0, 2.0, 3.0)
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             document_store_->Put(document));
+  DocHitInfo docHitInfo(put_result.new_document_id);
+
+  ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "max(getScorableProperty(\"aliasPerson\", \"frequencyScore\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "aliasPerson", {"person"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  double expected_score = 100 + std::max({1.0, 2.0, 3.0});
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<AdvancedScorer> scorer,
+      AdvancedScorer::Create(scoring_spec,
+                             /*default_score=*/10, kDefaultSemanticMetricType,
+                             document_store_.get(), schema_store_.get(),
+                             fake_clock_.GetSystemTimeMilliseconds(),
+                             /*join_children_fetcher=*/nullptr,
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
+  scorer->PrepareToScore(/*query_term_iterators=*/{});
+  EXPECT_THAT(scorer->GetScore(docHitInfo, /*query_it=*/nullptr),
+              DoubleNear(expected_score, kEps));
+}
+
+TEST_F(AdvancedScorerTest, GetScorableProperty_WithInt64List) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace", "uri")
+                               .SetSchema("person")
+                               .SetScore(100)
+                               .SetCreationTimestampMs(123)
+                               .AddInt64Property("contactTimes", 10, 20, 30)
+                               .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             document_store_->Put(document));
+  DocHitInfo docHitInfo(put_result.new_document_id);
+
+  ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "min(getScorableProperty(\"aliasPerson\", \"contactTimes\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "aliasPerson", {"person"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  double expected_score = 100 + std::min({10, 20, 30});
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<AdvancedScorer> scorer,
+      AdvancedScorer::Create(scoring_spec,
+                             /*default_score=*/10, kDefaultSemanticMetricType,
+                             document_store_.get(), schema_store_.get(),
+                             fake_clock_.GetSystemTimeMilliseconds(),
+                             /*join_children_fetcher=*/nullptr,
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
+  scorer->PrepareToScore(/*query_term_iterators=*/{});
+  EXPECT_THAT(scorer->GetScore(docHitInfo, /*query_it=*/nullptr),
+              DoubleNear(expected_score, kEps));
+}
+
+TEST_F(AdvancedScorerTest, GetScorableProperty_WithBoolean) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace", "uri")
+                               .SetSchema("person")
+                               .SetScore(100)
+                               .SetCreationTimestampMs(123)
+                               .AddBooleanProperty("isStarred", true)
+                               .AddInt64Property("contactTimes", 10, 20, 30)
+                               .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             document_store_->Put(document));
+  DocHitInfo docHitInfo(put_result.new_document_id);
+
+  ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "100 * avg(getScorableProperty(\"aliasPerson\", \"isStarred\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "aliasPerson", {"person"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  double expected_score = 100 + 100 * 1.0;
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<AdvancedScorer> scorer,
+      AdvancedScorer::Create(scoring_spec,
+                             /*default_score=*/10, kDefaultSemanticMetricType,
+                             document_store_.get(), schema_store_.get(),
+                             fake_clock_.GetSystemTimeMilliseconds(),
+                             /*join_children_fetcher=*/nullptr,
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
+  scorer->PrepareToScore(/*query_term_iterators=*/{});
+  EXPECT_THAT(scorer->GetScore(docHitInfo, /*query_it=*/nullptr),
+              DoubleNear(expected_score, kEps));
+}
+
+TEST_F(AdvancedScorerTest,
+       ScoreWithScorableProperty_ScoringSpecWithMultipleProperties) {
+  DocumentProto document =
+      DocumentBuilder()
+          .SetKey("namespace", "uri")
+          .SetSchema("person")
+          .SetScore(100)
+          .SetCreationTimestampMs(123)
+          .AddBooleanProperty("isStarred", false)
+          .AddInt64Property("contactTimes", 10, 20, 30)
+          .AddDoubleProperty("frequencyScore", 1.0, 2.0, 3.0)
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             document_store_->Put(document));
+  DocHitInfo docHitInfo(put_result.new_document_id);
+
+  ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "100 * avg(getScorableProperty(\"aliasPerson\", \"isStarred\")) + "
+      "10 * max(getScorableProperty(\"aliasPerson\", \"frequencyScore\")) + "
+      "10 * max(getScorableProperty(\"aliasPerson\", \"contactTimes\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "aliasPerson", {"person"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  double expected_score = 100 + 100 * 0 + 10 * std::max({1.0, 2.0, 3.0}) +
+                          10 * std::max({10, 20, 30});
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<AdvancedScorer> scorer,
+      AdvancedScorer::Create(scoring_spec,
+                             /*default_score=*/10, kDefaultSemanticMetricType,
+                             document_store_.get(), schema_store_.get(),
+                             fake_clock_.GetSystemTimeMilliseconds(),
+                             /*join_children_fetcher=*/nullptr,
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
+  scorer->PrepareToScore(/*query_term_iterators=*/{});
+  EXPECT_THAT(scorer->GetScore(docHitInfo, /*query_it=*/nullptr),
+              DoubleNear(expected_score, kEps));
+}
+
+TEST_F(AdvancedScorerTest,
+       ScoreWithScorableProperty_ScoringSpecWithMultipleSchemas) {
+  DocumentProto document =
+      DocumentBuilder()
+          .SetKey("namespace", "uri")
+          .SetSchema("person")
+          .SetScore(100)
+          .SetCreationTimestampMs(123)
+          .AddBooleanProperty("isStarred", false)
+          .AddDoubleProperty("frequencyScore", 1.0, 2.0, 3.0)
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             document_store_->Put(document));
+  DocHitInfo docHitInfo(put_result.new_document_id);
+
+  ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "100 * avg(getScorableProperty(\"aliasPerson\", \"isStarred\")) + "
+      "10 * max(getScorableProperty(\"aliasPerson\", \"frequencyScore\")) + "
+      "10 * sum(getScorableProperty(\"aliasMessage\", \"frequencyScore\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "aliasPerson", {"person"});
+  AddSchemaTypeAliasMap(&scoring_spec, "aliasMessage", {"message"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  double expected_score =
+      100 + 100 * 0 + 10 * std::max({1.0, 2.0, 3.0}) + 10 * 0;
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<AdvancedScorer> scorer,
+      AdvancedScorer::Create(scoring_spec,
+                             /*default_score=*/10, kDefaultSemanticMetricType,
+                             document_store_.get(), schema_store_.get(),
+                             fake_clock_.GetSystemTimeMilliseconds(),
+                             /*join_children_fetcher=*/nullptr,
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
+  scorer->PrepareToScore(/*query_term_iterators=*/{});
+  EXPECT_THAT(scorer->GetScore(docHitInfo, /*query_it=*/nullptr),
+              DoubleNear(expected_score, kEps));
+}
+
+TEST_F(AdvancedScorerTest,
+       ScoreWithScorableProperty_WithMathExpressionTakeEmptyList) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace", "uri")
+                               .SetSchema("email")
+                               .SetScore(100)
+                               .SetCreationTimestampMs(123)
+                               .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             document_store_->Put(document));
+  DocHitInfo docHitInfo(put_result.new_document_id);
+
+  // Expected score will fall back to the default score, because max() throws an
+  // error when it takes an empty list.
+  ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "100 * avg(getScorableProperty(\"aliasPerson\", \"isStarred\")) + "
+      "10 * max(getScorableProperty(\"aliasPerson\", \"frequencyScore\"))");
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  AddSchemaTypeAliasMap(&scoring_spec, "aliasPerson", {"person"});
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<AdvancedScorer> scorer,
+      AdvancedScorer::Create(scoring_spec,
+                             /*default_score=*/10, kDefaultSemanticMetricType,
+                             document_store_.get(), schema_store_.get(),
+                             fake_clock_.GetSystemTimeMilliseconds(),
+                             /*join_children_fetcher=*/nullptr,
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
+  scorer->PrepareToScore(/*query_term_iterators=*/{});
+  EXPECT_THAT(scorer->GetScore(docHitInfo, /*query_it=*/nullptr),
+              DoubleNear(10, kEps));
+}
+
+TEST_F(AdvancedScorerTest,
+       ScoreWithScorableProperty_MaxOrDefaultTakeEmptyList) {
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("namespace", "uri")
+                               .SetSchema("email")
+                               .SetScore(100)
+                               .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             document_store_->Put(document));
+  DocHitInfo docHitInfo(put_result.new_document_id);
+
+  ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "10 * maxOrDefault(getScorableProperty(\"aliasPerson\", "
+      "\"frequencyScore\"), "
+      "5)");
+  AddSchemaTypeAliasMap(&scoring_spec, "aliasPerson", {"person"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  double expected_score = 100 + 10 * 5;
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<AdvancedScorer> scorer,
+      AdvancedScorer::Create(scoring_spec,
+                             /*default_score=*/10, kDefaultSemanticMetricType,
+                             document_store_.get(), schema_store_.get(),
+                             fake_clock_.GetSystemTimeMilliseconds(),
+                             /*join_children_fetcher=*/nullptr,
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
+  scorer->PrepareToScore(/*query_term_iterators=*/{});
+  EXPECT_THAT(scorer->GetScore(docHitInfo, /*query_it=*/nullptr),
+              DoubleNear(expected_score, kEps));
+}
+
+TEST_F(AdvancedScorerTest, ScoreWithScorableProperty_WithNestedSchemas) {
+  SchemaProto schema_proto =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("person")
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("id")
+                                   .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                   .SetCardinality(CARDINALITY_REPEATED))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("income")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("age")
+                          .SetDataType(PropertyConfigProto::DataType::INT64)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("isStarred")
+                          .SetDataType(PropertyConfigProto::DataType::BOOLEAN)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_OPTIONAL)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("sender")
+                          .SetDataTypeDocument("person",
+                                               /*index_nested_properties=*/true)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("receiver")
+                          .SetDataTypeDocument("person",
+                                               /*index_nested_properties=*/true)
+                          .SetCardinality(CARDINALITY_REPEATED))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("importanceBoolean")
+                          .SetDataType(PropertyConfigProto::DataType::BOOLEAN)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("scoreDouble")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("scoreInt64")
+                                   .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                   .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                   .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+
+  ICING_ASSERT_OK(schema_store_->SetSchema(
+      schema_proto, /*ignore_errors_and_delete_documents=*/true,
+      /*allow_circular_schema_definitions=*/true));
+
+  DocumentProto document =
+      DocumentBuilder()
+          .SetKey("foo", "1")
+          .SetSchema("email")
+          .SetScore(100)
+          .AddStringProperty("subject", "subject foo")
+          .AddBooleanProperty("importanceBoolean", true)
+          .AddInt64Property("scoreInt64", 1, 2, 3)
+          .AddDocumentProperty(
+              "receiver",
+              DocumentBuilder()
+                  .SetKey("namespace", "uri1")
+                  .SetSchema("person")
+                  .AddInt64Property("age", 30)
+                  .AddDoubleProperty("income", 10000, 20000, 30000)
+                  .AddBooleanProperty("isStarred", true)
+                  .Build(),
+              DocumentBuilder()
+                  .SetKey("namespace", "uri2")
+                  .SetSchema("person")
+                  .AddInt64Property("age", 35)
+                  .AddDoubleProperty("income", 10001, 20001, 30001)
+                  .AddBooleanProperty("isStarred", false)
+                  .Build())
+          .AddDocumentProperty(
+              "sender", DocumentBuilder()
+                            .SetKey("namespace", "uri3")
+                            .SetSchema("person")
+                            .AddInt64Property("age", 50)
+                            .AddDoubleProperty("income", 21001, 21002, 21003)
+                            .AddBooleanProperty("isStarred", true)
+                            .Build())
+          .SetCreationTimestampMs(0)
+          .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             document_store_->Put(document));
+  DocHitInfo docHitInfo(put_result.new_document_id);
+  ScoringSpecProto scoring_spec1 = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "10 * max(getScorableProperty(\"aliasEmail\", \"scoreInt64\"))");
+  AddSchemaTypeAliasMap(&scoring_spec1, "aliasEmail", {"email"});
+  double expected_score1 = 100 + 10 * std::max({1, 2, 3});
+  scoring_spec1.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<AdvancedScorer> scorer,
+      AdvancedScorer::Create(scoring_spec1,
+                             /*default_score=*/0, kDefaultSemanticMetricType,
+                             document_store_.get(), schema_store_.get(),
+                             fake_clock_.GetSystemTimeMilliseconds(),
+                             /*join_children_fetcher=*/nullptr,
+                             &empty_embedding_query_results_,
+                             feature_flags_.get()));
+  scorer->PrepareToScore(/*query_term_iterators=*/{});
+  EXPECT_THAT(scorer->GetScore(docHitInfo, /*query_it=*/nullptr),
+              DoubleNear(expected_score1, kEps));
+
+  ScoringSpecProto scoring_spec2 = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "10 * min(getScorableProperty(\"aliasEmail\", \"receiver.age\"))");
+  AddSchemaTypeAliasMap(&scoring_spec2, "aliasEmail", {"email"});
+  scoring_spec2.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  double expected_score2 = 100 + 10 * std::min({30, 35});
+  ICING_ASSERT_OK_AND_ASSIGN(
+      scorer, AdvancedScorer::Create(
+                  scoring_spec2,
+                  /*default_score=*/0, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
+  scorer->PrepareToScore(/*query_term_iterators=*/{});
+  EXPECT_THAT(scorer->GetScore(docHitInfo, /*query_it=*/nullptr),
+              DoubleNear(expected_score2, kEps));
+
+  ScoringSpecProto scoring_spec3 = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "50 * min(getScorableProperty(\"aliasEmail\", \"importanceBoolean\")) + "
+      "10 * min(getScorableProperty(\"aliasEmail\", \"receiver.age\")) + "
+      "20 * min(getScorableProperty(\"aliasEmail\", \"sender.isStarred\")) + "
+      "5 * max(getScorableProperty(\"aliasEmail\", \"receiver.income\"))");
+  AddSchemaTypeAliasMap(&scoring_spec3, "aliasEmail", {"email"});
+  scoring_spec3.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+  double expected_score3 =
+      100 + 50 * 1 + 10 * std::min({30, 35}) + 20 * 1 +
+      5 * std::max({10000, 20000, 30000, 10001, 20001, 30001});
+  ICING_ASSERT_OK_AND_ASSIGN(
+      scorer, AdvancedScorer::Create(
+                  scoring_spec3,
+                  /*default_score=*/0, kDefaultSemanticMetricType,
+                  document_store_.get(), schema_store_.get(),
+                  fake_clock_.GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results_, feature_flags_.get()));
+  scorer->PrepareToScore(/*query_term_iterators=*/{});
+  EXPECT_THAT(scorer->GetScore(docHitInfo, /*query_it=*/nullptr),
+              DoubleNear(expected_score3, kEps));
+}
+
+TEST_F(AdvancedScorerTest, SchemaTypeAliasMap_AliasSchemaTypeNotMatched) {
+  ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "sum(getScorableProperty(\"aliasEmail\", \"frequencyScore\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "aliasPerson", {"person"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+
+  EXPECT_THAT(
+      AdvancedScorer::Create(
+          scoring_spec, /*default_score=*/10, kDefaultSemanticMetricType,
+          document_store_.get(), schema_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+               HasSubstr("The alias schema type in the score expression is not "
+                         "found in the schema_type_alias_map")));
+}
+
+TEST_F(AdvancedScorerTest,
+       SchemaTypeAliasMap_PropertyNotScorableForSomeSchemaTypes) {
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("pkg1/db1/message")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("frequencyScore")
+                          .SetDataType(PropertyConfigProto::DataType::INT64)
+                          .SetCardinality(CARDINALITY_REPEATED)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)))
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("pkg2/db1/message")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("frequencyScore")
+                          .SetDataType(PropertyConfigProto::DataType::INT64)
+                          .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+
+  ICING_ASSERT_OK(schema_store_->SetSchema(
+      schema, /*ignore_errors_and_delete_documents=*/true,
+      /*allow_circular_schema_definitions=*/false));
+
+  ScoringSpecProto scoring_spec = CreateAdvancedScoringSpec(
+      "this.documentScore() + "
+      "sum(getScorableProperty(\"message\", \"frequencyScore\"))");
+  AddSchemaTypeAliasMap(&scoring_spec, "message",
+                        {"pkg1/db1/message", "pkg2/db1/message"});
+  scoring_spec.add_scoring_feature_types_enabled(
+      ScoringFeatureType::SCORABLE_PROPERTY_RANKING);
+
+  EXPECT_THAT(
+      AdvancedScorer::Create(
+          scoring_spec, /*default_score=*/10, kDefaultSemanticMetricType,
+          document_store_.get(), schema_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results_,
+          feature_flags_.get()),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+               HasSubstr("'frequencyScore' is not defined as a scorable "
+                         "property under schema type 1")));
+}
+
 }  // namespace
 
 }  // namespace lib
diff --git a/icing/scoring/advanced_scoring/score-expression-util.h b/icing/scoring/advanced_scoring/score-expression-util.h
new file mode 100644
index 0000000..1756585
--- /dev/null
+++ b/icing/scoring/advanced_scoring/score-expression-util.h
@@ -0,0 +1,111 @@
+// Copyright (C) 2024 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_SCORE_EXPRESSION_UTIL_H_
+#define ICING_SCORING_ADVANCED_SCORING_SCORE_EXPRESSION_UTIL_H_
+
+#include <cstdint>
+#include <memory>
+#include <string_view>
+#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/feature-flags.h"
+#include "icing/index/embed/embedding-query-results.h"
+#include "icing/join/join-children-fetcher.h"
+#include "icing/proto/scoring.pb.h"
+#include "icing/query/advanced_query_parser/abstract-syntax-tree.h"
+#include "icing/query/advanced_query_parser/lexer.h"
+#include "icing/query/advanced_query_parser/parser.h"
+#include "icing/schema/schema-store.h"
+#include "icing/scoring/advanced_scoring/score-expression.h"
+#include "icing/scoring/advanced_scoring/scoring-visitor.h"
+#include "icing/scoring/bm25f-calculator.h"
+#include "icing/scoring/section-weights.h"
+#include "icing/store/document-store.h"
+#include "icing/util/status-macros.h"
+
+namespace icing {
+namespace lib {
+namespace score_expression_util {
+
+// Returns a ScoreExpression instance for the given scoring expression.
+//
+// join_children_fetcher, embedding_query_results, section_weights,
+// bm25f_calculator, schema_type_alias_map are allowed to be nullptr if the
+// corresponding scoring expression does not use them.
+//
+// Returns:
+//   - A ScoreExpression instance on success.
+//   - Any syntax or semantics error from Lexer, Parser, or ScoringVisitor.
+inline libtextclassifier3::StatusOr<std::unique_ptr<ScoreExpression>>
+GetScoreExpression(std::string_view scoring_expression, double default_score,
+                   SearchSpecProto::EmbeddingQueryMetricType::Code
+                       default_semantic_metric_type,
+                   const DocumentStore* document_store,
+                   const SchemaStore* schema_store, int64_t current_time_ms,
+                   const JoinChildrenFetcher* join_children_fetcher,
+                   const EmbeddingQueryResults* embedding_query_results,
+                   SectionWeights* section_weights,
+                   Bm25fCalculator* bm25f_calculator,
+                   const SchemaTypeAliasMap* schema_type_alias_map,
+                   const FeatureFlags* feature_flags,
+                   const std::unordered_set<ScoringFeatureType>*
+                       scoring_feature_types_enabled) {
+  ICING_RETURN_ERROR_IF_NULL(document_store);
+  ICING_RETURN_ERROR_IF_NULL(schema_store);
+  ICING_RETURN_ERROR_IF_NULL(feature_flags);
+
+  Lexer lexer(scoring_expression, Lexer::Language::SCORING);
+  ICING_ASSIGN_OR_RETURN(std::vector<Lexer::LexerToken> lexer_tokens,
+                         std::move(lexer).ExtractTokens());
+  Parser parser = Parser::Create(std::move(lexer_tokens));
+  ICING_ASSIGN_OR_RETURN(std::unique_ptr<Node> tree_root,
+                         parser.ConsumeScoring());
+  ScoringVisitor visitor(
+      default_score, default_semantic_metric_type, document_store, schema_store,
+      section_weights, bm25f_calculator, join_children_fetcher,
+      embedding_query_results, schema_type_alias_map, feature_flags,
+      scoring_feature_types_enabled, current_time_ms);
+  tree_root->Accept(&visitor);
+
+  ICING_ASSIGN_OR_RETURN(std::unique_ptr<ScoreExpression> expression,
+                         std::move(visitor).Expression());
+  if (expression->type() != ScoreExpressionType::kDouble) {
+    return absl_ports::InvalidArgumentError(
+        "The root scoring expression is not of double type.");
+  }
+  return expression;
+}
+
+inline std::unique_ptr<std::unordered_set<ScoringFeatureType>>
+GetEnabledScoringFeatureTypes(const ScoringSpecProto& scoring_spec) {
+  auto scoring_feature_types_enabled =
+      std::make_unique<std::unordered_set<ScoringFeatureType>>();
+  for (int feature_type : scoring_spec.scoring_feature_types_enabled()) {
+    scoring_feature_types_enabled->insert(
+        static_cast<ScoringFeatureType>(feature_type));
+  }
+  return scoring_feature_types_enabled;
+}
+
+}  // namespace score_expression_util
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_SCORING_ADVANCED_SCORING_SCORE_EXPRESSION_UTIL_H_
diff --git a/icing/scoring/advanced_scoring/score-expression-util_test.cc b/icing/scoring/advanced_scoring/score-expression-util_test.cc
new file mode 100644
index 0000000..29ef915
--- /dev/null
+++ b/icing/scoring/advanced_scoring/score-expression-util_test.cc
@@ -0,0 +1,283 @@
+// Copyright (C) 2024 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/score-expression-util.h"
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <unordered_set>
+#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/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/proto/document.pb.h"
+#include "icing/proto/schema.pb.h"
+#include "icing/proto/scoring.pb.h"
+#include "icing/proto/usage.pb.h"
+#include "icing/schema-builder.h"
+#include "icing/schema/schema-store.h"
+#include "icing/scoring/advanced_scoring/score-expression.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-feature-flags.h"
+#include "icing/testing/tmp-directory.h"
+
+namespace icing {
+namespace lib {
+
+namespace {
+
+using ::testing::DoubleEq;
+using ::testing::HasSubstr;
+
+class ScoreExpressionUtilTest : public testing::Test {
+ protected:
+  ScoreExpressionUtilTest()
+      : test_dir_(GetTestTempDir() + "/icing"),
+        doc_store_dir_(test_dir_ + "/doc_store"),
+        schema_store_dir_(test_dir_ + "/schema_store") {}
+
+  void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+    scoring_feature_types_enabled_ = {
+        ScoringFeatureType::SCORABLE_PROPERTY_RANKING};
+    filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
+    filesystem_.CreateDirectoryRecursively(doc_store_dir_.c_str());
+    filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str());
+
+    ICING_ASSERT_OK_AND_ASSIGN(
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
+
+    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));
+    document_store_ = std::move(create_result.document_store);
+
+    // Creates the schema
+    SchemaProto test_schema =
+        SchemaBuilder()
+            .AddType(SchemaTypeConfigBuilder().SetType("email").AddProperty(
+                PropertyConfigBuilder()
+                    .SetName("subject")
+                    .SetDataTypeString(
+                        TermMatchType::PREFIX,
+                        StringIndexingConfig::TokenizerType::PLAIN)
+                    .SetCardinality(CARDINALITY_OPTIONAL)))
+            .Build();
+
+    ICING_ASSERT_OK(schema_store_->SetSchema(
+        std::move(test_schema), /*ignore_errors_and_delete_documents=*/false,
+        /*allow_circular_schema_definitions=*/false));
+  }
+
+  void TearDown() override {
+    document_store_.reset();
+    schema_store_.reset();
+    filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
+  }
+
+  std::unique_ptr<FeatureFlags> feature_flags_;
+  std::unordered_set<ScoringFeatureType> scoring_feature_types_enabled_;
+  Filesystem filesystem_;
+  FakeClock fake_clock_;
+  const std::string test_dir_;
+  const std::string doc_store_dir_;
+  const std::string schema_store_dir_;
+  std::unique_ptr<SchemaStore> schema_store_;
+  std::unique_ptr<DocumentStore> document_store_;
+};
+
+constexpr int kDefaultScore = 0;
+constexpr SearchSpecProto::EmbeddingQueryMetricType::Code
+    kDefaultSemanticMetricType =
+        SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT;
+
+DocumentProto CreateDocument(const std::string& name_space,
+                             const std::string& uri, int score,
+                             int64_t creation_timestamp_ms) {
+  return DocumentBuilder()
+      .SetKey(name_space, uri)
+      .SetSchema("email")
+      .SetScore(score)
+      .SetCreationTimestampMs(creation_timestamp_ms)
+      .Build();
+}
+
+TEST_F(ScoreExpressionUtilTest,
+       FunctionsAreBannedIfMissingCorrespondingDependencies) {
+  EXPECT_THAT(
+      score_expression_util::GetScoreExpression(
+          "len(this.childrenRankingSignals())", kDefaultScore,
+          kDefaultSemanticMetricType, document_store_.get(),
+          schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr,
+          /*embedding_query_results=*/nullptr, /*section_weights=*/nullptr,
+          /*bm25f_calculator=*/nullptr, /*schema_type_alias_map=*/nullptr,
+          feature_flags_.get(), &scoring_feature_types_enabled_),
+      StatusIs(
+          libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+          HasSubstr("childrenRankingSignals must only be used with join")));
+
+  EXPECT_THAT(
+      score_expression_util::GetScoreExpression(
+          "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))",
+          kDefaultScore, kDefaultSemanticMetricType, document_store_.get(),
+          schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr,
+          /*embedding_query_results=*/nullptr, /*section_weights=*/nullptr,
+          /*bm25f_calculator=*/nullptr, /*schema_type_alias_map=*/nullptr,
+          feature_flags_.get(), &scoring_feature_types_enabled_),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+               HasSubstr("matchedSemanticScores function is not "
+                         "available in this context")));
+
+  EXPECT_THAT(
+      score_expression_util::GetScoreExpression(
+          "sum(this.propertyWeights())", kDefaultScore,
+          kDefaultSemanticMetricType, document_store_.get(),
+          schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr,
+          /*embedding_query_results=*/nullptr, /*section_weights=*/nullptr,
+          /*bm25f_calculator=*/nullptr, /*schema_type_alias_map=*/nullptr,
+          feature_flags_.get(), &scoring_feature_types_enabled_),
+      StatusIs(
+          libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+          HasSubstr(
+              "propertyWeights function is not available in this context")));
+
+  EXPECT_THAT(
+      score_expression_util::GetScoreExpression(
+          "this.relevanceScore()", kDefaultScore, kDefaultSemanticMetricType,
+          document_store_.get(), schema_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr,
+          /*embedding_query_results=*/nullptr, /*section_weights=*/nullptr,
+          /*bm25f_calculator=*/nullptr, /*schema_type_alias_map=*/nullptr,
+          feature_flags_.get(), &scoring_feature_types_enabled_),
+      StatusIs(
+          libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+          HasSubstr(
+              "relevanceScore function is not available in this context")));
+
+  EXPECT_THAT(
+      score_expression_util::GetScoreExpression(
+          "sum(getScorableProperty(\"aliasPerson\", \"contactTimes\"))",
+          kDefaultScore, kDefaultSemanticMetricType, document_store_.get(),
+          schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr,
+          /*embedding_query_results=*/nullptr, /*section_weights=*/nullptr,
+          /*bm25f_calculator=*/nullptr, /*schema_type_alias_map=*/nullptr,
+          feature_flags_.get(), &scoring_feature_types_enabled_),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT,
+               HasSubstr("getScorableProperty function is not "
+                         "available in this context")));
+}
+
+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)));
+  DocumentId document_id = put_result.new_document_id;
+  DocHitInfo doc_hit_info = DocHitInfo(document_id);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<ScoreExpression> score_expression,
+      score_expression_util::GetScoreExpression(
+          "1 + 1", kDefaultScore, kDefaultSemanticMetricType,
+          document_store_.get(), schema_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr,
+          /*embedding_query_results=*/nullptr, /*section_weights=*/nullptr,
+          /*bm25f_calculator=*/nullptr, /*schema_type_alias_map=*/nullptr,
+          feature_flags_.get(), &scoring_feature_types_enabled_));
+
+  EXPECT_THAT(
+      score_expression->EvaluateDouble(doc_hit_info, /*query_it=*/nullptr),
+      IsOkAndHolds(DoubleEq(2)));
+}
+
+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)));
+  DocumentId document_id = put_result.new_document_id;
+  DocHitInfo doc_hit_info = DocHitInfo(document_id);
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<ScoreExpression> score_expression,
+      score_expression_util::GetScoreExpression(
+          "this.documentScore()", kDefaultScore, kDefaultSemanticMetricType,
+          document_store_.get(), schema_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr,
+          /*embedding_query_results=*/nullptr, /*section_weights=*/nullptr,
+          /*bm25f_calculator=*/nullptr, /*schema_type_alias_map=*/nullptr,
+          feature_flags_.get(), &scoring_feature_types_enabled_));
+  EXPECT_THAT(
+      score_expression->EvaluateDouble(doc_hit_info, /*query_it=*/nullptr),
+      IsOkAndHolds(DoubleEq(10)));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      score_expression,
+      score_expression_util::GetScoreExpression(
+          "this.creationTimestamp()", kDefaultScore, kDefaultSemanticMetricType,
+          document_store_.get(), schema_store_.get(),
+          fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr,
+          /*embedding_query_results=*/nullptr, /*section_weights=*/nullptr,
+          /*bm25f_calculator=*/nullptr, /*schema_type_alias_map=*/nullptr,
+          feature_flags_.get(), &scoring_feature_types_enabled_));
+  EXPECT_THAT(
+      score_expression->EvaluateDouble(doc_hit_info, /*query_it=*/nullptr),
+      IsOkAndHolds(DoubleEq(100)));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      score_expression,
+      score_expression_util::GetScoreExpression(
+          "this.documentScore() + this.creationTimestamp()", kDefaultScore,
+          kDefaultSemanticMetricType, document_store_.get(),
+          schema_store_.get(), fake_clock_.GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr,
+          /*embedding_query_results=*/nullptr, /*section_weights=*/nullptr,
+          /*bm25f_calculator=*/nullptr, /*schema_type_alias_map=*/nullptr,
+          feature_flags_.get(), &scoring_feature_types_enabled_));
+  EXPECT_THAT(
+      score_expression->EvaluateDouble(doc_hit_info, /*query_it=*/nullptr),
+      IsOkAndHolds(DoubleEq(10 + 100)));
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/scoring/advanced_scoring/score-expression.cc b/icing/scoring/advanced_scoring/score-expression.cc
index 8bd151e..8804605 100644
--- a/icing/scoring/advanced_scoring/score-expression.cc
+++ b/icing/scoring/advanced_scoring/score-expression.cc
@@ -37,6 +37,9 @@
 #include "icing/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
 #include "icing/join/join-children-fetcher.h"
+#include "icing/legacy/core/icing-string-util.h"
+#include "icing/proto/internal/scorable_property_set.pb.h"
+#include "icing/schema/schema-store.h"
 #include "icing/schema/section.h"
 #include "icing/scoring/bm25f-calculator.h"
 #include "icing/scoring/scored-document-hit.h"
@@ -47,6 +50,7 @@
 #include "icing/store/document-store.h"
 #include "icing/util/embedding-util.h"
 #include "icing/util/logging.h"
+#include "icing/util/scorable_property_set.h"
 #include "icing/util/status-macros.h"
 
 namespace icing {
@@ -62,6 +66,23 @@
   return libtextclassifier3::Status::OK;
 }
 
+SchemaTypeId GetSchemaTypeId(DocumentId document_id,
+                             const DocumentStore& document_store,
+                             int64_t current_time_ms) {
+  auto filter_data_optional =
+      document_store.GetAliveDocumentFilterData(document_id, current_time_ms);
+  if (!filter_data_optional) {
+    // This should never happen. The only failure case for
+    // GetAliveDocumentFilterData is if the document_id is outside of the range
+    // of allocated document_ids, which shouldn't be possible since we're
+    // getting this document_id from the posting lists.
+    ICING_LOG(WARNING) << "No document filter data for document ["
+                       << document_id << "]";
+    return kInvalidSchemaTypeId;
+  }
+  return filter_data_optional.value().schema_type_id();
+}
+
 }  // namespace
 
 libtextclassifier3::StatusOr<std::unique_ptr<ScoreExpression>>
@@ -586,7 +607,8 @@
     std::unique_ptr<ChildrenRankingSignalsFunctionScoreExpression>>
 ChildrenRankingSignalsFunctionScoreExpression::Create(
     std::vector<std::unique_ptr<ScoreExpression>> args,
-    const JoinChildrenFetcher* join_children_fetcher) {
+    const DocumentStore& document_store,
+    const JoinChildrenFetcher* join_children_fetcher, int64_t current_time_ms) {
   if (args.size() != 1) {
     return absl_ports::InvalidArgumentError(
         "childrenRankingSignals must have 1 argument.");
@@ -600,12 +622,11 @@
   if (join_children_fetcher == nullptr) {
     return absl_ports::InvalidArgumentError(
         "childrenRankingSignals must only be used with join, but "
-        "JoinChildrenFetcher "
-        "is not provided.");
+        "JoinChildrenFetcher is not provided.");
   }
   return std::unique_ptr<ChildrenRankingSignalsFunctionScoreExpression>(
       new ChildrenRankingSignalsFunctionScoreExpression(
-          *join_children_fetcher));
+          document_store, *join_children_fetcher, current_time_ms));
 }
 
 libtextclassifier3::StatusOr<std::vector<double>>
@@ -648,7 +669,8 @@
     const DocHitInfo& hit_info, const DocHitInfoIterator*) const {
   std::vector<double> weights;
   SectionIdMask sections = hit_info.hit_section_ids_mask();
-  SchemaTypeId schema_type_id = GetSchemaTypeId(hit_info.document_id());
+  SchemaTypeId schema_type_id = GetSchemaTypeId(
+      hit_info.document_id(), document_store_, current_time_ms_);
 
   while (sections != 0) {
     SectionId section_id = __builtin_ctzll(sections);
@@ -659,22 +681,6 @@
   return weights;
 }
 
-SchemaTypeId PropertyWeightsFunctionScoreExpression::GetSchemaTypeId(
-    DocumentId document_id) const {
-  auto filter_data_optional =
-      document_store_.GetAliveDocumentFilterData(document_id, current_time_ms_);
-  if (!filter_data_optional) {
-    // This should never happen. The only failure case for
-    // GetAliveDocumentFilterData is if the document_id is outside of the range
-    // of allocated document_ids, which shouldn't be possible since we're
-    // getting this document_id from the posting lists.
-    ICING_LOG(WARNING) << "No document filter data for document ["
-                       << document_id << "]";
-    return kInvalidSchemaTypeId;
-  }
-  return filter_data_optional.value().schema_type_id();
-}
-
 libtextclassifier3::StatusOr<std::unique_ptr<ScoreExpression>>
 GetEmbeddingParameterFunctionScoreExpression::Create(
     std::vector<std::unique_ptr<ScoreExpression>> args) {
@@ -795,5 +801,139 @@
   return *scores;
 }
 
+GetScorablePropertyFunctionScoreExpression::
+    GetScorablePropertyFunctionScoreExpression(
+        const DocumentStore* document_store, const SchemaStore* schema_store,
+        int64_t current_time_ms,
+        std::unordered_set<SchemaTypeId>&& schema_type_ids,
+        std::string_view property_path)
+    : document_store_(*document_store),
+      schema_store_(*schema_store),
+      current_time_ms_(current_time_ms),
+      schema_type_ids_(std::move(schema_type_ids)),
+      property_path_(property_path) {}
+
+libtextclassifier3::StatusOr<std::unordered_set<SchemaTypeId>>
+GetScorablePropertyFunctionScoreExpression::GetAndValidateSchemaTypeIds(
+    std::string_view alias_schema_type, std::string_view property_path,
+    const SchemaTypeAliasMap& schema_type_alias_map,
+    const SchemaStore& schema_store) {
+  auto alias_map_iter = schema_type_alias_map.find(alias_schema_type.data());
+  if (alias_map_iter == schema_type_alias_map.end()) {
+    return absl_ports::InvalidArgumentError(absl_ports::StrCat(
+        "The alias schema type in the score expression is not found in the "
+        "schema_type_alias_map: ",
+        alias_schema_type));
+  }
+
+  std::unordered_set<SchemaTypeId> schema_type_ids;
+  for (std::string_view schema_type : alias_map_iter->second) {
+    // First, verify that the schema type has a valid schema type id in the
+    // schema store.
+    libtextclassifier3::StatusOr<SchemaTypeId> schema_type_id_or =
+        schema_store.GetSchemaTypeId(schema_type);
+    if (!schema_type_id_or.ok()) {
+      if (absl_ports::IsNotFound(schema_type_id_or.status())) {
+        // Ignores the schema type if it is not found in the schema store.
+        continue;
+      }
+      return schema_type_id_or.status();
+    }
+    SchemaTypeId schema_type_id = schema_type_id_or.ValueOrDie();
+
+    // Then, calls GetScorablePropertyIndex() here to validate if the property
+    // path is scorable under the schema type, no need to check the returned
+    // index value.
+    libtextclassifier3::StatusOr<std::optional<int>>
+        scorable_property_index_or = schema_store.GetScorablePropertyIndex(
+            schema_type_id, property_path);
+    if (!scorable_property_index_or.ok()) {
+      return scorable_property_index_or.status();
+    }
+    if (!scorable_property_index_or.ValueOrDie().has_value()) {
+      return absl_ports::InvalidArgumentError(IcingStringUtil::StringPrintf(
+          "'%s' is not defined as a scorable property under schema type %d",
+          property_path.data(), schema_type_id));
+    }
+    schema_type_ids.insert(schema_type_id);
+  }
+  return schema_type_ids;
+}
+
+libtextclassifier3::StatusOr<
+    std::unique_ptr<GetScorablePropertyFunctionScoreExpression>>
+GetScorablePropertyFunctionScoreExpression::Create(
+    std::vector<std::unique_ptr<ScoreExpression>> args,
+    const DocumentStore* document_store, const SchemaStore* schema_store,
+    const SchemaTypeAliasMap& schema_type_alias_map, int64_t current_time_ms) {
+  ICING_RETURN_IF_ERROR(CheckChildrenNotNull(args));
+
+  if (args.size() != 2 || args[0]->type() != ScoreExpressionType::kString ||
+      args[1]->type() != ScoreExpressionType::kString) {
+    return absl_ports::InvalidArgumentError(absl_ports::StrCat(
+        kFunctionName, " must take exactly two string params"));
+  }
+
+  // Validate schema type.
+  ICING_ASSIGN_OR_RETURN(std::string_view alias_schema_type,
+                         args[0]->EvaluateString());
+  ICING_ASSIGN_OR_RETURN(std::string_view property_path,
+                         args[1]->EvaluateString());
+  ICING_ASSIGN_OR_RETURN(
+      std::unordered_set<SchemaTypeId> schema_type_ids,
+      GetAndValidateSchemaTypeIds(alias_schema_type, property_path,
+                                  schema_type_alias_map, *schema_store));
+
+  return std::unique_ptr<GetScorablePropertyFunctionScoreExpression>(
+      new GetScorablePropertyFunctionScoreExpression(
+          document_store, schema_store, current_time_ms,
+          std::move(schema_type_ids), property_path));
+}
+
+libtextclassifier3::StatusOr<std::vector<double>>
+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>();
+  }
+
+  std::unique_ptr<ScorablePropertySet> scorable_property_set =
+      document_store_.GetScorablePropertySet(hit_info.document_id(),
+                                             current_time_ms_);
+  // It should never happen.
+  if (scorable_property_set == nullptr) {
+    return absl_ports::InternalError(IcingStringUtil::StringPrintf(
+        "Failed to retrieve ScorablePropertySet for document %d",
+        hit_info.document_id()));
+  }
+
+  const ScorablePropertyProto* scorable_property_proto =
+      scorable_property_set->GetScorablePropertyProto(property_path_);
+  // It should never happen as icing generates a default value for each scorable
+  // property when the document is created.
+  if (scorable_property_proto == nullptr) {
+    return absl_ports::InternalError(IcingStringUtil::StringPrintf(
+        "Failed to retrieve ScorablePropertyProto for document %d, and "
+        "property path %s",
+        hit_info.document_id(), property_path_.c_str()));
+  }
+
+  // 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());
+  } 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());
+  } 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 std::vector<double>();
+}
+
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/scoring/advanced_scoring/score-expression.h b/icing/scoring/advanced_scoring/score-expression.h
index 2bb5871..b61ac7b 100644
--- a/icing/scoring/advanced_scoring/score-expression.h
+++ b/icing/scoring/advanced_scoring/score-expression.h
@@ -30,6 +30,7 @@
 #include "icing/index/hit/doc-hit-info.h"
 #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/bm25f-calculator.h"
 #include "icing/scoring/section-weights.h"
 #include "icing/store/document-filter-data.h"
@@ -50,6 +51,10 @@
   kString,
 };
 
+// A map from alias schema type to a set of Icing schema types.
+using SchemaTypeAliasMap =
+    std::unordered_map<std::string, std::unordered_set<std::string>>;
+
 class ScoreExpression {
  public:
   virtual ~ScoreExpression() = default;
@@ -372,7 +377,9 @@
   static libtextclassifier3::StatusOr<
       std::unique_ptr<ChildrenRankingSignalsFunctionScoreExpression>>
   Create(std::vector<std::unique_ptr<ScoreExpression>> args,
-         const JoinChildrenFetcher* join_children_fetcher);
+         const DocumentStore& document_store,
+         const JoinChildrenFetcher* join_children_fetcher,
+         int64_t current_time_ms);
 
   libtextclassifier3::StatusOr<std::vector<double>> EvaluateList(
       const DocHitInfo& hit_info,
@@ -384,9 +391,15 @@
 
  private:
   explicit ChildrenRankingSignalsFunctionScoreExpression(
-      const JoinChildrenFetcher& join_children_fetcher)
-      : join_children_fetcher_(join_children_fetcher) {}
-  const JoinChildrenFetcher& join_children_fetcher_;
+      const DocumentStore& document_store,
+      const JoinChildrenFetcher& join_children_fetcher, int64_t current_time_ms)
+      : document_store_(document_store),
+        join_children_fetcher_(join_children_fetcher),
+        current_time_ms_(current_time_ms) {}
+
+  const DocumentStore& document_store_;               // Does not own.
+  const JoinChildrenFetcher& join_children_fetcher_;  // Does not own.
+  int64_t current_time_ms_;
 };
 
 class PropertyWeightsFunctionScoreExpression : public ScoreExpression {
@@ -410,8 +423,6 @@
     return ScoreExpressionType::kDoubleList;
   }
 
-  SchemaTypeId GetSchemaTypeId(DocumentId document_id) const;
-
  private:
   explicit PropertyWeightsFunctionScoreExpression(
       const DocumentStore* document_store,
@@ -488,6 +499,63 @@
   const EmbeddingQueryResults& embedding_query_results_;
 };
 
+class GetScorablePropertyFunctionScoreExpression : public ScoreExpression {
+ public:
+  static constexpr std::string_view kFunctionName = "getScorableProperty";
+
+  // Returns:
+  //   - FAILED_PRECONDITION on any null pointer in children.
+  //   - INVALID_ARGUMENT on
+  //     - |args| type errors.
+  //     - alias_schema_type in the scoring expression does not match to any
+  //       schema type in the |schema_type_alias_map|.
+  //     - any matched schema type not having the specified property_path as a
+  //       scorable property.
+  static libtextclassifier3::StatusOr<
+      std::unique_ptr<GetScorablePropertyFunctionScoreExpression>>
+  Create(std::vector<std::unique_ptr<ScoreExpression>> args,
+         const DocumentStore* document_store, const SchemaStore* schema_store,
+         const SchemaTypeAliasMap& schema_type_alias_map,
+         int64_t current_time_ms);
+
+  ScoreExpressionType type() const override {
+    return ScoreExpressionType::kDoubleList;
+  }
+
+  libtextclassifier3::StatusOr<std::vector<double>> EvaluateList(
+      const DocHitInfo& hit_info,
+      const DocHitInfoIterator* query_it) const override;
+
+ private:
+  explicit GetScorablePropertyFunctionScoreExpression(
+      const DocumentStore* document_store, const SchemaStore* schema_store,
+      int64_t current_time_ms,
+      std::unordered_set<SchemaTypeId>&& schema_type_ids,
+      std::string_view property_path);
+
+  // Returns a set of schema type ids that are matched to the
+  // |alias_schema_type| in the scoring expression, based on the
+  // |schema_type_alias_map|.
+  //
+  // For each of the schema type in the returned set, this function also
+  // validates that:
+  //   - The schema type is valid in the schema store.
+  //   - The |property_path| is defined as scorable under the schema type.
+  static libtextclassifier3::StatusOr<std::unordered_set<SchemaTypeId>>
+  GetAndValidateSchemaTypeIds(std::string_view alias_schema_type,
+                              std::string_view property_path,
+                              const SchemaTypeAliasMap& schema_type_alias_map,
+                              const SchemaStore& schema_store);
+
+  const DocumentStore& document_store_;
+  const SchemaStore& schema_store_;
+  int64_t current_time_ms_;
+  // A doc hit is evaluated by this function only if its schema type id is in
+  // this set.
+  std::unordered_set<SchemaTypeId> schema_type_ids_;
+  std::string property_path_;
+};
+
 }  // namespace lib
 }  // namespace icing
 
diff --git a/icing/scoring/advanced_scoring/scoring-visitor.cc b/icing/scoring/advanced_scoring/scoring-visitor.cc
index 4d38c0e..eeab302 100644
--- a/icing/scoring/advanced_scoring/scoring-visitor.cc
+++ b/icing/scoring/advanced_scoring/scoring-visitor.cc
@@ -106,18 +106,31 @@
   } else if (function_name ==
              RelevanceScoreFunctionScoreExpression::kFunctionName) {
     // relevanceScore function
-    expression = RelevanceScoreFunctionScoreExpression::Create(
-        std::move(args), &bm25f_calculator_, default_score_);
+    if (bm25f_calculator_ != nullptr) {
+      expression = RelevanceScoreFunctionScoreExpression::Create(
+          std::move(args), bm25f_calculator_, default_score_);
+    } else {
+      expression = absl_ports::InvalidArgumentError(
+          "relevanceScore function is not available in this context.");
+    }
+
   } else if (function_name ==
              ChildrenRankingSignalsFunctionScoreExpression::kFunctionName) {
     // childrenRankingSignals function
     expression = ChildrenRankingSignalsFunctionScoreExpression::Create(
-        std::move(args), join_children_fetcher_);
+        std::move(args), document_store_, join_children_fetcher_,
+        current_time_ms_);
   } else if (function_name ==
              PropertyWeightsFunctionScoreExpression::kFunctionName) {
     // propertyWeights function
-    expression = PropertyWeightsFunctionScoreExpression::Create(
-        std::move(args), &document_store_, &section_weights_, current_time_ms_);
+    if (section_weights_ != nullptr) {
+      expression = PropertyWeightsFunctionScoreExpression::Create(
+          std::move(args), &document_store_, section_weights_,
+          current_time_ms_);
+    } else {
+      expression = absl_ports::InvalidArgumentError(
+          "propertyWeights function is not available in this context.");
+    }
   } else if (MathFunctionScoreExpression::kFunctionNames.find(function_name) !=
              MathFunctionScoreExpression::kFunctionNames.end()) {
     // Math functions
@@ -132,16 +145,40 @@
         ListOperationFunctionScoreExpression::kFunctionNames.at(function_name),
         std::move(args));
   } else if (function_name ==
-                 GetEmbeddingParameterFunctionScoreExpression::kFunctionName) {
+             GetEmbeddingParameterFunctionScoreExpression::kFunctionName) {
     // getEmbeddingParameter function
     expression =
         GetEmbeddingParameterFunctionScoreExpression::Create(std::move(args));
   } else if (function_name ==
              MatchedSemanticScoresFunctionScoreExpression::kFunctionName) {
     // matchedSemanticScores function
-    expression = MatchedSemanticScoresFunctionScoreExpression::Create(
-        std::move(args), default_semantic_metric_type_,
-        &embedding_query_results_);
+    if (embedding_query_results_ != nullptr) {
+      expression = MatchedSemanticScoresFunctionScoreExpression::Create(
+          std::move(args), default_semantic_metric_type_,
+          embedding_query_results_);
+    } else {
+      expression = absl_ports::InvalidArgumentError(
+          "matchedSemanticScores function is not available in this context.");
+    }
+  } else if (function_name ==
+             GetScorablePropertyFunctionScoreExpression::kFunctionName) {
+    if (!feature_flags_.enable_scorable_properties()) {
+      expression = absl_ports::InvalidArgumentError(
+          "getScorableProperty function is not enabled.");
+    } else if (scoring_feature_types_enabled_.find(
+                   ScoringFeatureType::SCORABLE_PROPERTY_RANKING) ==
+               scoring_feature_types_enabled_.end()) {
+      expression = absl_ports::InvalidArgumentError(
+          "SCORABLE_PROPERTY_RANKING feature is not enabled.");
+    } else if (schema_type_alias_map_ == nullptr) {
+      expression = absl_ports::InvalidArgumentError(
+          "getScorableProperty function is not available in this context.");
+    } else {
+      // getScorableProperty function
+      expression = GetScorablePropertyFunctionScoreExpression::Create(
+          std::move(args), &document_store_, &schema_store_,
+          *schema_type_alias_map_, current_time_ms_);
+    }
   }
 
   if (!expression.ok()) {
diff --git a/icing/scoring/advanced_scoring/scoring-visitor.h b/icing/scoring/advanced_scoring/scoring-visitor.h
index 3c4a374..44a4d80 100644
--- a/icing/scoring/advanced_scoring/scoring-visitor.h
+++ b/icing/scoring/advanced_scoring/scoring-visitor.h
@@ -17,15 +17,18 @@
 
 #include <cstdint>
 #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/feature-flags.h"
 #include "icing/index/embed/embedding-query-results.h"
 #include "icing/join/join-children-fetcher.h"
 #include "icing/legacy/core/icing-string-util.h"
+#include "icing/proto/scoring.pb.h"
 #include "icing/query/advanced_query_parser/abstract-syntax-tree.h"
 #include "icing/schema/schema-store.h"
 #include "icing/scoring/advanced_scoring/score-expression.h"
@@ -38,6 +41,9 @@
 
 class ScoringVisitor : public AbstractSyntaxTreeVisitor {
  public:
+  // join_children_fetcher, embedding_query_results, section_weights,
+  // bm25f_calculator, schema_type_alias_map are allowed to be nullptr if the
+  // corresponding scoring expression does not use them.
   explicit ScoringVisitor(double default_score,
                           SearchSpecProto::EmbeddingQueryMetricType::Code
                               default_semantic_metric_type,
@@ -47,15 +53,22 @@
                           Bm25fCalculator* bm25f_calculator,
                           const JoinChildrenFetcher* join_children_fetcher,
                           const EmbeddingQueryResults* embedding_query_results,
+                          const SchemaTypeAliasMap* schema_type_alias_map,
+                          const FeatureFlags* feature_flags,
+                          const std::unordered_set<ScoringFeatureType>*
+                              scoring_feature_types_enabled,
                           int64_t current_time_ms)
       : default_score_(default_score),
         default_semantic_metric_type_(default_semantic_metric_type),
         document_store_(*document_store),
         schema_store_(*schema_store),
-        section_weights_(*section_weights),
-        bm25f_calculator_(*bm25f_calculator),
+        section_weights_(section_weights),
+        bm25f_calculator_(bm25f_calculator),
         join_children_fetcher_(join_children_fetcher),
-        embedding_query_results_(*embedding_query_results),
+        embedding_query_results_(embedding_query_results),
+        schema_type_alias_map_(schema_type_alias_map),
+        feature_flags_(*feature_flags),
+        scoring_feature_types_enabled_(*scoring_feature_types_enabled),
         current_time_ms_(current_time_ms) {}
 
   void VisitString(const StringNode* node) override;
@@ -104,13 +117,20 @@
   double default_score_;
   const SearchSpecProto::EmbeddingQueryMetricType::Code
       default_semantic_metric_type_;
-  const DocumentStore& document_store_;
-  const SchemaStore& schema_store_;
-  SectionWeights& section_weights_;
-  Bm25fCalculator& bm25f_calculator_;
+  const DocumentStore& document_store_;  // Does not own.
+  const SchemaStore& schema_store_;      // Does not own.
+
+  SectionWeights* section_weights_;    // nullable, does not own.
+  Bm25fCalculator* bm25f_calculator_;  // nullable, does not own.
   // A non-null join_children_fetcher_ indicates scoring in a join.
-  const JoinChildrenFetcher* join_children_fetcher_;  // Does not own.
-  const EmbeddingQueryResults& embedding_query_results_;
+  const JoinChildrenFetcher* join_children_fetcher_;  // nullable, does not own.
+  const EmbeddingQueryResults*
+      embedding_query_results_;                      // nullable, does not own.
+  const SchemaTypeAliasMap* schema_type_alias_map_;  // nullable, does not own.
+
+  const FeatureFlags& feature_flags_;  // Does not own.
+  const std::unordered_set<ScoringFeatureType>&
+      scoring_feature_types_enabled_;  // Does not own.
 
   libtextclassifier3::Status pending_error_;
   std::vector<std::unique_ptr<ScoreExpression>> stack_;
diff --git a/icing/scoring/bm25f-calculator.cc b/icing/scoring/bm25f-calculator.cc
index a80ef34..e32d4e6 100644
--- a/icing/scoring/bm25f-calculator.cc
+++ b/icing/scoring/bm25f-calculator.cc
@@ -14,19 +14,26 @@
 
 #include "icing/scoring/bm25f-calculator.h"
 
+#include <cmath>
 #include <cstdint>
-#include <cstdlib>
+#include <memory>
 #include <string>
-#include <unordered_set>
+#include <string_view>
+#include <unordered_map>
 #include <vector>
 
 #include "icing/index/hit/doc-hit-info.h"
+#include "icing/index/hit/hit.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
+#include "icing/schema/section.h"
+#include "icing/scoring/section-weights.h"
 #include "icing/store/corpus-associated-scoring-data.h"
 #include "icing/store/corpus-id.h"
 #include "icing/store/document-associated-score-data.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"
 
 namespace icing {
 namespace lib {
@@ -150,6 +157,11 @@
 
   uint32_t num_docs = csdata.num_docs();
   uint32_t nqi = corpus_nqi_map_[corpus_term_info.value];
+  if (nqi > num_docs) {
+    ICING_LOG(ERROR) << "nqi > num_docs when calculating idf for corpus "
+                     << corpus_id << " term " << term;
+    return 0;
+  }
   float idf =
       nqi != 0 ? log(1.0f + (num_docs - nqi + 0.5f) / (nqi + 0.5f)) : 0.0f;
   corpus_idf_map_.insert({corpus_term_info.value, idf});
diff --git a/icing/scoring/bm25f-calculator.h b/icing/scoring/bm25f-calculator.h
index 36f9c68..d3f4354 100644
--- a/icing/scoring/bm25f-calculator.h
+++ b/icing/scoring/bm25f-calculator.h
@@ -16,14 +16,19 @@
 #define ICING_SCORING_BM25F_CALCULATOR_H_
 
 #include <cstdint>
+#include <memory>
 #include <string>
-#include <unordered_set>
-#include <vector>
+#include <string_view>
+#include <unordered_map>
 
+#include "icing/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
 #include "icing/legacy/index/icing-bit-util.h"
 #include "icing/scoring/section-weights.h"
 #include "icing/store/corpus-id.h"
+#include "icing/store/document-associated-score-data.h"
+#include "icing/store/document-filter-data.h"
+#include "icing/store/document-id.h"
 #include "icing/store/document-store.h"
 
 namespace icing {
@@ -92,13 +97,13 @@
   // Compact representation of <CorpusId, TermId> for use as a key in a
   // hash_map.
   struct CorpusTermInfo {
-    // Layout bits: 16 bit CorpusId + 16 bit TermId
-    using Value = uint32_t;
+    // Layout bits: 16 bit padding + 32 bit CorpusId + 16 bit TermId
+    using Value = uint64_t;
 
     Value value;
 
-    static constexpr int kCorpusIdBits = sizeof(CorpusId);
-    static constexpr int kTermIdBits = sizeof(TermId);
+    static constexpr int kCorpusIdBits = sizeof(CorpusId) * 8;
+    static constexpr int kTermIdBits = sizeof(TermId) * 8;
 
     explicit CorpusTermInfo(CorpusId corpus_id, TermId term_id) : value(0) {
       BITFIELD_OR(value, kTermIdBits, kCorpusIdBits,
diff --git a/icing/scoring/score-and-rank_benchmark.cc b/icing/scoring/score-and-rank_benchmark.cc
index b203b6f..57c02ef 100644
--- a/icing/scoring/score-and-rank_benchmark.cc
+++ b/icing/scoring/score-and-rank_benchmark.cc
@@ -25,6 +25,7 @@
 #include "testing/base/public/benchmark.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/index/embed/embedding-query-results.h"
@@ -42,6 +43,7 @@
 #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"
 
@@ -96,17 +98,18 @@
 
 libtextclassifier3::StatusOr<DocumentStore::CreateResult> CreateDocumentStore(
     const Filesystem* filesystem, const std::string& base_dir,
-    const Clock* clock, const SchemaStore* schema_store) {
+    const Clock* clock, const SchemaStore* schema_store,
+    const FeatureFlags& feature_flags) {
   return DocumentStore::Create(
-      filesystem, base_dir, clock, schema_store,
+      filesystem, base_dir, clock, schema_store, &feature_flags,
       /*force_recovery_and_revalidate_documents=*/false,
-      /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-      /*use_persistent_hash_map=*/true,
-      PortableFileBackedProtoLog<DocumentWrapper>::kDeflateCompressionLevel,
+      /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
+      PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
       /*initialize_stats=*/nullptr);
 }
 
 void BM_ScoreAndRankDocumentHitsByDocumentScore(benchmark::State& state) {
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   const std::string base_dir = GetTestTempDir() + "/score_and_rank_benchmark";
   const std::string document_store_dir = base_dir + "/document_store";
   const std::string schema_store_dir = base_dir + "/schema_store";
@@ -119,14 +122,14 @@
   ASSERT_TRUE(filesystem.CreateDirectoryRecursively(schema_store_dir.c_str()));
 
   Clock clock;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem, schema_store_dir, &clock));
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<SchemaStore> schema_store,
+                             SchemaStore::Create(&filesystem, schema_store_dir,
+                                                 &clock, &feature_flags));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem, document_store_dir, &clock,
-                          schema_store.get()));
+                          schema_store.get(), feature_flags));
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
@@ -144,7 +147,7 @@
           SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT,
           document_store.get(), schema_store.get(),
           clock.GetSystemTimeMilliseconds(), /*join_children_fetcher=*/nullptr,
-          &empty_embedding_query_results));
+          &empty_embedding_query_results, &feature_flags));
   int num_to_score = state.range(0);
   int num_of_documents = state.range(1);
 
@@ -217,6 +220,7 @@
     ->ArgPair(10000, 20000);
 
 void BM_ScoreAndRankDocumentHitsByCreationTime(benchmark::State& state) {
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   const std::string base_dir = GetTestTempDir() + "/score_and_rank_benchmark";
   const std::string document_store_dir = base_dir + "/document_store";
   const std::string schema_store_dir = base_dir + "/schema_store";
@@ -229,14 +233,14 @@
   ASSERT_TRUE(filesystem.CreateDirectoryRecursively(schema_store_dir.c_str()));
 
   Clock clock;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem, schema_store_dir, &clock));
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<SchemaStore> schema_store,
+                             SchemaStore::Create(&filesystem, schema_store_dir,
+                                                 &clock, &feature_flags));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem, document_store_dir, &clock,
-                          schema_store.get()));
+                          schema_store.get(), feature_flags));
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
@@ -255,7 +259,7 @@
           SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT,
           document_store.get(), schema_store.get(),
           clock.GetSystemTimeMilliseconds(), /*join_children_fetcher=*/nullptr,
-          &empty_embedding_query_results));
+          &empty_embedding_query_results, &feature_flags));
 
   int num_to_score = state.range(0);
   int num_of_documents = state.range(1);
@@ -330,6 +334,7 @@
     ->ArgPair(10000, 20000);
 
 void BM_ScoreAndRankDocumentHitsNoScoring(benchmark::State& state) {
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   const std::string base_dir = GetTestTempDir() + "/score_and_rank_benchmark";
   const std::string document_store_dir = base_dir + "/document_store";
   const std::string schema_store_dir = base_dir + "/schema_store";
@@ -342,14 +347,14 @@
   ASSERT_TRUE(filesystem.CreateDirectoryRecursively(schema_store_dir.c_str()));
 
   Clock clock;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem, schema_store_dir, &clock));
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<SchemaStore> schema_store,
+                             SchemaStore::Create(&filesystem, schema_store_dir,
+                                                 &clock, &feature_flags));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem, document_store_dir, &clock,
-                          schema_store.get()));
+                          schema_store.get(), feature_flags));
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
@@ -367,7 +372,7 @@
           SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT,
           document_store.get(), schema_store.get(),
           clock.GetSystemTimeMilliseconds(), /*join_children_fetcher=*/nullptr,
-          &empty_embedding_query_results));
+          &empty_embedding_query_results, &feature_flags));
 
   int num_to_score = state.range(0);
   int num_of_documents = state.range(1);
@@ -437,6 +442,7 @@
     ->ArgPair(10000, 20000);
 
 void BM_ScoreAndRankDocumentHitsByRelevanceScoring(benchmark::State& state) {
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   const std::string base_dir = GetTestTempDir() + "/score_and_rank_benchmark";
   const std::string document_store_dir = base_dir + "/document_store";
   const std::string schema_store_dir = base_dir + "/schema_store";
@@ -449,14 +455,14 @@
   ASSERT_TRUE(filesystem.CreateDirectoryRecursively(schema_store_dir.c_str()));
 
   Clock clock;
-  ICING_ASSERT_OK_AND_ASSIGN(
-      std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem, schema_store_dir, &clock));
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<SchemaStore> schema_store,
+                             SchemaStore::Create(&filesystem, schema_store_dir,
+                                                 &clock, &feature_flags));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem, document_store_dir, &clock,
-                          schema_store.get()));
+                          schema_store.get(), feature_flags));
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
@@ -474,7 +480,7 @@
           SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT,
           document_store.get(), schema_store.get(),
           clock.GetSystemTimeMilliseconds(), /*join_children_fetcher=*/nullptr,
-          &empty_embedding_query_results));
+          &empty_embedding_query_results, &feature_flags));
 
   int num_to_score = state.range(0);
   int num_of_documents = state.range(1);
diff --git a/icing/scoring/scorer-factory.cc b/icing/scoring/scorer-factory.cc
index 7fc9efb..afda649 100644
--- a/icing/scoring/scorer-factory.cc
+++ b/icing/scoring/scorer-factory.cc
@@ -23,6 +23,7 @@
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/absl_ports/canonical_errors.h"
+#include "icing/feature-flags.h"
 #include "icing/index/embed/embedding-query-results.h"
 #include "icing/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
@@ -184,10 +185,12 @@
         default_semantic_metric_type,
     const DocumentStore* document_store, const SchemaStore* schema_store,
     int64_t current_time_ms, const JoinChildrenFetcher* join_children_fetcher,
-    const EmbeddingQueryResults* embedding_query_results) {
+    const EmbeddingQueryResults* embedding_query_results,
+    const FeatureFlags* feature_flags) {
   ICING_RETURN_ERROR_IF_NULL(document_store);
   ICING_RETURN_ERROR_IF_NULL(schema_store);
   ICING_RETURN_ERROR_IF_NULL(embedding_query_results);
+  ICING_RETURN_ERROR_IF_NULL(feature_flags);
 
   if (scoring_spec.rank_by() !=
       ScoringSpecProto::RankingStrategy::ADVANCED_SCORING_EXPRESSION) {
@@ -243,7 +246,7 @@
       return AdvancedScorer::Create(
           scoring_spec, default_score, default_semantic_metric_type,
           document_store, schema_store, current_time_ms, join_children_fetcher,
-          embedding_query_results);
+          embedding_query_results, feature_flags);
     case ScoringSpecProto::RankingStrategy::JOIN_AGGREGATE_SCORE:
       // Use join aggregate score to rank. Since the aggregation score is
       // calculated by child documents after joining (in JoinProcessor), we can
diff --git a/icing/scoring/scorer-factory.h b/icing/scoring/scorer-factory.h
index f5766b3..7687e98 100644
--- a/icing/scoring/scorer-factory.h
+++ b/icing/scoring/scorer-factory.h
@@ -19,6 +19,7 @@
 #include <memory>
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/feature-flags.h"
 #include "icing/index/embed/embedding-query-results.h"
 #include "icing/join/join-children-fetcher.h"
 #include "icing/schema/schema-store.h"
@@ -46,7 +47,8 @@
         default_semantic_metric_type,
     const DocumentStore* document_store, const SchemaStore* schema_store,
     int64_t current_time_ms, const JoinChildrenFetcher* join_children_fetcher,
-    const EmbeddingQueryResults* embedding_query_results);
+    const EmbeddingQueryResults* embedding_query_results,
+    const FeatureFlags* feature_flags);
 
 }  // namespace scorer_factory
 
diff --git a/icing/scoring/scorer_test.cc b/icing/scoring/scorer_test.cc
index 097d0c7..a027a94 100644
--- a/icing/scoring/scorer_test.cc
+++ b/icing/scoring/scorer_test.cc
@@ -24,6 +24,7 @@
 #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/index/embed/embedding-query-results.h"
@@ -40,6 +41,7 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
+#include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 
 namespace icing {
@@ -56,6 +58,7 @@
         schema_store_dir_(test_dir_ + "/schema_store") {}
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
     filesystem_.CreateDirectoryRecursively(doc_store_dir_.c_str());
     filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str());
@@ -65,18 +68,19 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(
         schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock1_));
+        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock1_,
+                            feature_flags_.get()));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(
-            &filesystem_, doc_store_dir_, &fake_clock1_, schema_store_.get(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
 
     // Creates a simple email schema
@@ -116,7 +120,7 @@
       SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT;
   EmbeddingQueryResults empty_embedding_query_results;
 
- private:
+  std::unique_ptr<FeatureFlags> feature_flags_;
   const std::string test_dir_;
   const std::string doc_store_dir_;
   const std::string schema_store_dir_;
@@ -146,7 +150,8 @@
           /*default_score=*/0, default_semantic_metric_type,
           /*document_store=*/nullptr, schema_store(),
           fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()),
       StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 }
 
@@ -157,7 +162,8 @@
               ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE, GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           /*schema_store=*/nullptr, fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()),
       StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 }
 
@@ -169,7 +175,8 @@
               ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE, GetParam()),
           /*default_score=*/10, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   // Non existent document id
   DocHitInfo docHitInfo = DocHitInfo(/*document_id_in=*/1);
@@ -197,7 +204,8 @@
               ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE, GetParam()),
           /*default_score=*/10, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   DocHitInfo docHitInfo = DocHitInfo(document_id);
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(0));
@@ -224,7 +232,8 @@
               ScoringSpecProto::RankingStrategy::DOCUMENT_SCORE, GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   DocHitInfo docHitInfo = DocHitInfo(document_id);
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(5));
@@ -253,7 +262,8 @@
               ScoringSpecProto::RankingStrategy::RELEVANCE_SCORE, GetParam()),
           /*default_score=*/10, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   DocHitInfo docHitInfo = DocHitInfo(document_id);
   EXPECT_THAT(scorer->GetScore(docHitInfo), Eq(10));
@@ -291,7 +301,8 @@
               GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   DocHitInfo docHitInfo1 = DocHitInfo(document_id1);
   DocHitInfo docHitInfo2 = DocHitInfo(document_id2);
@@ -322,7 +333,8 @@
               ScoringSpecProto::RankingStrategy::USAGE_TYPE1_COUNT, GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Scorer> scorer2,
       scorer_factory::Create(
@@ -330,7 +342,8 @@
               ScoringSpecProto::RankingStrategy::USAGE_TYPE2_COUNT, GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Scorer> scorer3,
       scorer_factory::Create(
@@ -338,7 +351,8 @@
               ScoringSpecProto::RankingStrategy::USAGE_TYPE3_COUNT, GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   DocHitInfo docHitInfo = DocHitInfo(document_id);
   EXPECT_THAT(scorer1->GetScore(docHitInfo), Eq(0));
   EXPECT_THAT(scorer2->GetScore(docHitInfo), Eq(0));
@@ -376,7 +390,8 @@
               ScoringSpecProto::RankingStrategy::USAGE_TYPE1_COUNT, GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Scorer> scorer2,
       scorer_factory::Create(
@@ -384,7 +399,8 @@
               ScoringSpecProto::RankingStrategy::USAGE_TYPE2_COUNT, GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Scorer> scorer3,
       scorer_factory::Create(
@@ -392,7 +408,8 @@
               ScoringSpecProto::RankingStrategy::USAGE_TYPE3_COUNT, GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   DocHitInfo docHitInfo = DocHitInfo(document_id);
   EXPECT_THAT(scorer1->GetScore(docHitInfo), Eq(0));
   EXPECT_THAT(scorer2->GetScore(docHitInfo), Eq(0));
@@ -430,7 +447,8 @@
               ScoringSpecProto::RankingStrategy::USAGE_TYPE1_COUNT, GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Scorer> scorer2,
       scorer_factory::Create(
@@ -438,7 +456,8 @@
               ScoringSpecProto::RankingStrategy::USAGE_TYPE2_COUNT, GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Scorer> scorer3,
       scorer_factory::Create(
@@ -446,7 +465,8 @@
               ScoringSpecProto::RankingStrategy::USAGE_TYPE3_COUNT, GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   DocHitInfo docHitInfo = DocHitInfo(document_id);
   EXPECT_THAT(scorer1->GetScore(docHitInfo), Eq(0));
   EXPECT_THAT(scorer2->GetScore(docHitInfo), Eq(0));
@@ -486,7 +506,8 @@
               GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Scorer> scorer2,
       scorer_factory::Create(
@@ -496,7 +517,8 @@
               GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Scorer> scorer3,
       scorer_factory::Create(
@@ -506,7 +528,8 @@
               GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   DocHitInfo docHitInfo = DocHitInfo(document_id);
   EXPECT_THAT(scorer1->GetScore(docHitInfo), Eq(0));
   EXPECT_THAT(scorer2->GetScore(docHitInfo), Eq(0));
@@ -562,7 +585,8 @@
               GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Scorer> scorer2,
       scorer_factory::Create(
@@ -572,7 +596,8 @@
               GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Scorer> scorer3,
       scorer_factory::Create(
@@ -582,7 +607,8 @@
               GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   DocHitInfo docHitInfo = DocHitInfo(document_id);
   EXPECT_THAT(scorer1->GetScore(docHitInfo), Eq(0));
   EXPECT_THAT(scorer2->GetScore(docHitInfo), Eq(0));
@@ -638,7 +664,8 @@
               GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Scorer> scorer2,
       scorer_factory::Create(
@@ -648,7 +675,8 @@
               GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<Scorer> scorer3,
       scorer_factory::Create(
@@ -658,7 +686,8 @@
               GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   DocHitInfo docHitInfo = DocHitInfo(document_id);
   EXPECT_THAT(scorer1->GetScore(docHitInfo), Eq(0));
   EXPECT_THAT(scorer2->GetScore(docHitInfo), Eq(0));
@@ -699,7 +728,8 @@
               ScoringSpecProto::RankingStrategy::NONE, GetParam()),
           /*default_score=*/3, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   DocHitInfo docHitInfo1 = DocHitInfo(/*document_id_in=*/0);
   DocHitInfo docHitInfo2 = DocHitInfo(/*document_id_in=*/1);
@@ -715,7 +745,8 @@
               ScoringSpecProto::RankingStrategy::NONE, GetParam()),
           /*default_score=*/111, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   docHitInfo1 = DocHitInfo(/*document_id_in=*/4);
   docHitInfo2 = DocHitInfo(/*document_id_in=*/5);
@@ -747,7 +778,8 @@
               GetParam()),
           /*default_score=*/0, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock1().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   DocHitInfo docHitInfo = DocHitInfo(document_id);
 
   // Create usage report for the maximum allowable timestamp.
diff --git a/icing/scoring/scoring-processor.cc b/icing/scoring/scoring-processor.cc
index db38a6f..4cba8a6 100644
--- a/icing/scoring/scoring-processor.cc
+++ b/icing/scoring/scoring-processor.cc
@@ -24,6 +24,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/embed/embedding-query-results.h"
 #include "icing/index/hit/doc-hit-info.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
@@ -53,22 +54,23 @@
                          const SchemaStore* schema_store,
                          int64_t current_time_ms,
                          const JoinChildrenFetcher* join_children_fetcher,
-                         const EmbeddingQueryResults* embedding_query_results) {
+                         const EmbeddingQueryResults* embedding_query_results,
+                         const FeatureFlags* feature_flags) {
   ICING_RETURN_ERROR_IF_NULL(document_store);
   ICING_RETURN_ERROR_IF_NULL(schema_store);
   ICING_RETURN_ERROR_IF_NULL(embedding_query_results);
+  ICING_RETURN_ERROR_IF_NULL(feature_flags);
 
-  bool is_descending_order =
-      scoring_spec.order_by() == ScoringSpecProto::Order::DESC;
-
+  double default_score =
+      (scoring_spec.order_by() == ScoringSpecProto::Order::DESC)
+          ? kDefaultScoreInDescendingOrder
+          : kDefaultScoreInAscendingOrder;
   ICING_ASSIGN_OR_RETURN(
       std::unique_ptr<Scorer> scorer,
       scorer_factory::Create(
-          scoring_spec,
-          is_descending_order ? kDefaultScoreInDescendingOrder
-                              : kDefaultScoreInAscendingOrder,
-          default_semantic_metric_type, document_store, schema_store,
-          current_time_ms, join_children_fetcher, embedding_query_results));
+          scoring_spec, default_score, default_semantic_metric_type,
+          document_store, schema_store, current_time_ms, join_children_fetcher,
+          embedding_query_results, feature_flags));
   // Using `new` to access a non-public constructor.
   return std::unique_ptr<ScoringProcessor>(
       new ScoringProcessor(std::move(scorer)));
diff --git a/icing/scoring/scoring-processor.h b/icing/scoring/scoring-processor.h
index de0b95c..40a5912 100644
--- a/icing/scoring/scoring-processor.h
+++ b/icing/scoring/scoring-processor.h
@@ -23,6 +23,7 @@
 #include <vector>
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/feature-flags.h"
 #include "icing/index/embed/embedding-query-results.h"
 #include "icing/index/iterator/doc-hit-info-iterator.h"
 #include "icing/join/join-children-fetcher.h"
@@ -52,7 +53,8 @@
           default_semantic_metric_type,
       const DocumentStore* document_store, const SchemaStore* schema_store,
       int64_t current_time_ms, const JoinChildrenFetcher* join_children_fetcher,
-      const EmbeddingQueryResults* embedding_query_results);
+      const EmbeddingQueryResults* embedding_query_results,
+      const FeatureFlags* feature_flags);
 
   // Assigns scores to DocHitInfos from the given DocHitInfoIterator and returns
   // a vector of ScoredDocumentHits. The size of results is no more than
diff --git a/icing/scoring/scoring-processor_test.cc b/icing/scoring/scoring-processor_test.cc
index 0e46464..7949cdf 100644
--- a/icing/scoring/scoring-processor_test.cc
+++ b/icing/scoring/scoring-processor_test.cc
@@ -26,6 +26,7 @@
 #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/index/embed/embedding-query-results.h"
@@ -46,6 +47,7 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
+#include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 #include "icing/util/status-macros.h"
 
@@ -68,25 +70,26 @@
         schema_store_dir_(test_dir_ + "/schema_store") {}
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     // Creates file directories
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
     filesystem_.CreateDirectoryRecursively(doc_store_dir_.c_str());
     filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str());
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(
-            &filesystem_, doc_store_dir_, &fake_clock_, schema_store_.get(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/true,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
 
     // Creates a simple email schema
@@ -132,7 +135,7 @@
       SearchSpecProto::EmbeddingQueryMetricType::DOT_PRODUCT;
   EmbeddingQueryResults empty_embedding_query_results;
 
- private:
+  std::unique_ptr<FeatureFlags> feature_flags_;
   const std::string test_dir_;
   const std::string doc_store_dir_;
   const std::string schema_store_dir_;
@@ -209,12 +212,13 @@
 
 TEST_F(ScoringProcessorTest, CreationWithNullDocumentStoreShouldFail) {
   ScoringSpecProto spec_proto;
-  EXPECT_THAT(
-      ScoringProcessor::Create(
-          spec_proto, default_semantic_metric_type, /*document_store=*/nullptr,
-          schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results),
-      StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
+  EXPECT_THAT(ScoringProcessor::Create(
+                  spec_proto, default_semantic_metric_type,
+                  /*document_store=*/nullptr, schema_store(),
+                  fake_clock().GetSystemTimeMilliseconds(),
+                  /*join_children_fetcher=*/nullptr,
+                  &empty_embedding_query_results, feature_flags_.get()),
+              StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 }
 
 TEST_F(ScoringProcessorTest, CreationWithNullSchemaStoreShouldFail) {
@@ -223,7 +227,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           /*schema_store=*/nullptr, fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()),
       StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
 }
 
@@ -233,7 +238,8 @@
   ICING_EXPECT_OK(ScoringProcessor::Create(
       spec_proto, default_semantic_metric_type, document_store(),
       schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+      /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+      feature_flags_.get()));
 }
 
 TEST_P(ScoringProcessorTest, ShouldHandleEmptyDocHitIterator) {
@@ -251,7 +257,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   EXPECT_THAT(scoring_processor->Score(std::move(doc_hit_info_iterator),
                                        /*num_to_score=*/5),
@@ -281,7 +288,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   EXPECT_THAT(scoring_processor->Score(std::move(doc_hit_info_iterator),
                                        /*num_to_score=*/-1),
@@ -314,7 +322,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   EXPECT_THAT(scoring_processor->Score(std::move(doc_hit_info_iterator),
                                        /*num_to_score=*/2),
@@ -349,7 +358,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   EXPECT_THAT(scoring_processor->Score(std::move(doc_hit_info_iterator),
                                        /*num_to_score=*/3),
@@ -410,7 +420,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   std::unordered_map<std::string, std::unique_ptr<DocHitInfoIterator>>
       query_term_iterators;
@@ -485,7 +496,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   std::unordered_map<std::string, std::unique_ptr<DocHitInfoIterator>>
       query_term_iterators;
@@ -564,7 +576,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   std::unordered_map<std::string, std::unique_ptr<DocHitInfoIterator>>
       query_term_iterators;
@@ -586,6 +599,232 @@
                   EqualsScoredDocumentHit(expected_scored_doc_hit3)));
 }
 
+TEST_P(ScoringProcessorTest, ShouldScoreByRelevanceScore_MultipleQueryTerms) {
+  DocumentProto document1 =
+      CreateDocument("icing", "email/1", kDefaultScore,
+                     /*creation_timestamp_ms=*/kDefaultCreationTimestampMs);
+  DocumentProto document2 =
+      CreateDocument("icing", "email/2", kDefaultScore,
+                     /*creation_timestamp_ms=*/kDefaultCreationTimestampMs);
+  DocumentProto document3 =
+      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));
+  DocumentId document_id1 = put_result1.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::PutResult put_result2,
+      document_store()->Put(document2, /*num_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));
+  DocumentId document_id3 = put_result3.new_document_id;
+
+  // Index 5 terms with total frequencies:
+  // {"foo": 7, "bar": 5, "baz": 2, "qux": 1, "lux": 4}
+  // Document 1 term-frequencies: {"foo": 3, "bar": 3, "baz": 2, "qux": 0,
+  // "lux": 3}
+  DocHitInfoTermFrequencyPair foo_doc_hit_info1 = DocHitInfo(document_id1);
+  DocHitInfoTermFrequencyPair bar_doc_hit_info1 = DocHitInfo(document_id1);
+  DocHitInfoTermFrequencyPair baz_doc_hit_info1 = DocHitInfo(document_id1);
+  DocHitInfoTermFrequencyPair lux_doc_hit_info1 = DocHitInfo(document_id1);
+  foo_doc_hit_info1.UpdateSection(/*section_id*/ 0, /*hit_term_frequency=*/3);
+  bar_doc_hit_info1.UpdateSection(/*section_id*/ 1, /*hit_term_frequency=*/3);
+  baz_doc_hit_info1.UpdateSection(/*section_id*/ 0, /*hit_term_frequency=*/2);
+  lux_doc_hit_info1.UpdateSection(/*section_id*/ 1, /*hit_term_frequency=*/3);
+  // Document 2 term-frequencies: {"foo": 3, "bar": 2, "baz": 0, "qux": 0,
+  // "lux": 0}
+  DocHitInfoTermFrequencyPair foo_doc_hit_info2 = DocHitInfo(document_id2);
+  DocHitInfoTermFrequencyPair bar_doc_hit_info2 = DocHitInfo(document_id2);
+  foo_doc_hit_info2.UpdateSection(/*section_id*/ 0, /*hit_term_frequency=*/3);
+  bar_doc_hit_info2.UpdateSection(/*section_id*/ 1, /*hit_term_frequency=*/2);
+  // Document 3 term-frequencies: {"foo": 1, "bar": 0, "baz": 0, "qux": 1,
+  // "lux": 1}
+  DocHitInfoTermFrequencyPair foo_doc_hit_info3 = DocHitInfo(document_id3);
+  DocHitInfoTermFrequencyPair qux_doc_hit_info3 = DocHitInfo(document_id3);
+  DocHitInfoTermFrequencyPair lux_doc_hit_info3 = DocHitInfo(document_id3);
+  foo_doc_hit_info3.UpdateSection(/*section_id*/ 0, /*hit_term_frequency=*/1);
+  qux_doc_hit_info3.UpdateSection(/*section_id*/ 1, /*hit_term_frequency=*/1);
+  lux_doc_hit_info3.UpdateSection(/*section_id*/ 1, /*hit_term_frequency=*/1);
+
+  // Creates input doc_hit_infos and expected output scored_document_hits
+  std::vector<DocHitInfoTermFrequencyPair> foo_doc_hit_infos = {
+      foo_doc_hit_info1, foo_doc_hit_info2, foo_doc_hit_info3};
+  std::vector<DocHitInfoTermFrequencyPair> bar_doc_hit_infos = {
+      bar_doc_hit_info1, bar_doc_hit_info2};
+  std::vector<DocHitInfoTermFrequencyPair> baz_doc_hit_infos = {
+      baz_doc_hit_info1};
+  std::vector<DocHitInfoTermFrequencyPair> qux_doc_hit_infos = {
+      qux_doc_hit_info3};
+  std::vector<DocHitInfoTermFrequencyPair> lux_doc_hit_infos = {
+      lux_doc_hit_info1, lux_doc_hit_info3};
+
+  // Creates a dummy DocHitInfoIterator with the results for the query words
+  std::unique_ptr<DocHitInfoIterator> foo_doc_hit_info_iterator =
+      std::make_unique<DocHitInfoIteratorDummy>(foo_doc_hit_infos, "foo");
+  std::unique_ptr<DocHitInfoIterator> bar_doc_hit_info_iterator =
+      std::make_unique<DocHitInfoIteratorDummy>(bar_doc_hit_infos, "bar");
+  std::unique_ptr<DocHitInfoIterator> baz_doc_hit_info_iterator =
+      std::make_unique<DocHitInfoIteratorDummy>(baz_doc_hit_infos, "baz");
+  std::unique_ptr<DocHitInfoIterator> qux_doc_hit_info_iterator =
+      std::make_unique<DocHitInfoIteratorDummy>(qux_doc_hit_infos, "qux");
+  std::unique_ptr<DocHitInfoIterator> lux_doc_hit_info_iterator =
+      std::make_unique<DocHitInfoIteratorDummy>(lux_doc_hit_infos, "lux");
+
+  ScoringSpecProto spec_proto = CreateScoringSpecForRankingStrategy(
+      ScoringSpecProto::RankingStrategy::RELEVANCE_SCORE, GetParam());
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<ScoringProcessor> scoring_processor,
+      ScoringProcessor::Create(
+          spec_proto, default_semantic_metric_type, document_store(),
+          schema_store(), fake_clock().GetSystemTimeMilliseconds(),
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
+
+  SectionIdMask kSectionIdMask1 = 0b00000001;
+  SectionIdMask kSectionIdMask2 = 0b00000010;
+
+  {
+    std::unordered_map<std::string, std::unique_ptr<DocHitInfoIterator>>
+        query_term_iterators;
+    query_term_iterators["foo"] =
+        std::make_unique<DocHitInfoIteratorDummy>(foo_doc_hit_infos, "foo");
+    query_term_iterators["bar"] =
+        std::make_unique<DocHitInfoIteratorDummy>(bar_doc_hit_infos, "bar");
+    query_term_iterators["baz"] =
+        std::make_unique<DocHitInfoIteratorDummy>(baz_doc_hit_infos, "baz");
+    query_term_iterators["qux"] =
+        std::make_unique<DocHitInfoIteratorDummy>(qux_doc_hit_infos, "qux");
+    query_term_iterators["lux"] =
+        std::make_unique<DocHitInfoIteratorDummy>(lux_doc_hit_infos, "lux");
+
+    // Score "foo".
+    ScoredDocumentHit foo_expected_scored_doc_hit1(document_id1,
+                                                   kSectionIdMask1,
+                                                   /*score=*/0.19672);
+    ScoredDocumentHit foo_expected_scored_doc_hit2(document_id2,
+                                                   kSectionIdMask1,
+                                                   /*score=*/0.19672);
+    ScoredDocumentHit foo_expected_scored_doc_hit3(document_id3,
+                                                   kSectionIdMask1,
+                                                   /*score=*/0.118455);
+    EXPECT_THAT(
+        scoring_processor->Score(std::move(foo_doc_hit_info_iterator),
+                                 /*num_to_score=*/3, &query_term_iterators),
+        ElementsAre(EqualsScoredDocumentHit(foo_expected_scored_doc_hit1),
+                    EqualsScoredDocumentHit(foo_expected_scored_doc_hit2),
+                    EqualsScoredDocumentHit(foo_expected_scored_doc_hit3)));
+  }
+
+  {
+    std::unordered_map<std::string, std::unique_ptr<DocHitInfoIterator>>
+        query_term_iterators;
+    query_term_iterators["foo"] =
+        std::make_unique<DocHitInfoIteratorDummy>(foo_doc_hit_infos, "foo");
+    query_term_iterators["bar"] =
+        std::make_unique<DocHitInfoIteratorDummy>(bar_doc_hit_infos, "bar");
+    query_term_iterators["baz"] =
+        std::make_unique<DocHitInfoIteratorDummy>(baz_doc_hit_infos, "baz");
+    query_term_iterators["qux"] =
+        std::make_unique<DocHitInfoIteratorDummy>(qux_doc_hit_infos, "qux");
+    query_term_iterators["lux"] =
+        std::make_unique<DocHitInfoIteratorDummy>(lux_doc_hit_infos, "lux");
+
+    // Score "bar"
+    ScoredDocumentHit bar_expected_scored_doc_hit1(document_id1,
+                                                   kSectionIdMask2,
+                                                   /*score=*/0.692416);
+    ScoredDocumentHit bar_expected_scored_doc_hit2(document_id2,
+                                                   kSectionIdMask2,
+                                                   /*score=*/0.594257);
+    EXPECT_THAT(
+        scoring_processor->Score(std::move(bar_doc_hit_info_iterator),
+                                 /*num_to_score=*/3, &query_term_iterators),
+        ElementsAre(EqualsScoredDocumentHit(bar_expected_scored_doc_hit1),
+                    EqualsScoredDocumentHit(bar_expected_scored_doc_hit2)));
+  }
+
+  {
+    std::unordered_map<std::string, std::unique_ptr<DocHitInfoIterator>>
+        query_term_iterators;
+    query_term_iterators["foo"] =
+        std::make_unique<DocHitInfoIteratorDummy>(foo_doc_hit_infos, "foo");
+    query_term_iterators["bar"] =
+        std::make_unique<DocHitInfoIteratorDummy>(bar_doc_hit_infos, "bar");
+    query_term_iterators["baz"] =
+        std::make_unique<DocHitInfoIteratorDummy>(baz_doc_hit_infos, "baz");
+    query_term_iterators["qux"] =
+        std::make_unique<DocHitInfoIteratorDummy>(qux_doc_hit_infos, "qux");
+    query_term_iterators["lux"] =
+        std::make_unique<DocHitInfoIteratorDummy>(lux_doc_hit_infos, "lux");
+
+    // Score "baz"
+    ScoredDocumentHit baz_expected_scored_doc_hit1(document_id1,
+                                                   kSectionIdMask1,
+                                                   /*score=*/1.240129);
+    EXPECT_THAT(
+        scoring_processor->Score(std::move(baz_doc_hit_info_iterator),
+                                 /*num_to_score=*/3, &query_term_iterators),
+        ElementsAre(EqualsScoredDocumentHit(baz_expected_scored_doc_hit1)));
+  }
+
+  {
+    std::unordered_map<std::string, std::unique_ptr<DocHitInfoIterator>>
+        query_term_iterators;
+    query_term_iterators["foo"] =
+        std::make_unique<DocHitInfoIteratorDummy>(foo_doc_hit_infos, "foo");
+    query_term_iterators["bar"] =
+        std::make_unique<DocHitInfoIteratorDummy>(bar_doc_hit_infos, "bar");
+    query_term_iterators["baz"] =
+        std::make_unique<DocHitInfoIteratorDummy>(baz_doc_hit_infos, "baz");
+    query_term_iterators["qux"] =
+        std::make_unique<DocHitInfoIteratorDummy>(qux_doc_hit_infos, "qux");
+    query_term_iterators["lux"] =
+        std::make_unique<DocHitInfoIteratorDummy>(lux_doc_hit_infos, "lux");
+
+    // Score "qux"
+    ScoredDocumentHit qux_expected_scored_doc_hit1(document_id3,
+                                                   kSectionIdMask2,
+                                                   /*score=*/0.87009);
+    EXPECT_THAT(
+        scoring_processor->Score(std::move(qux_doc_hit_info_iterator),
+                                 /*num_to_score=*/3, &query_term_iterators),
+        ElementsAre(EqualsScoredDocumentHit(qux_expected_scored_doc_hit1)));
+  }
+
+  {
+    std::unordered_map<std::string, std::unique_ptr<DocHitInfoIterator>>
+        query_term_iterators;
+    query_term_iterators["foo"] =
+        std::make_unique<DocHitInfoIteratorDummy>(foo_doc_hit_infos, "foo");
+    query_term_iterators["bar"] =
+        std::make_unique<DocHitInfoIteratorDummy>(bar_doc_hit_infos, "bar");
+    query_term_iterators["baz"] =
+        std::make_unique<DocHitInfoIteratorDummy>(baz_doc_hit_infos, "baz");
+    query_term_iterators["qux"] =
+        std::make_unique<DocHitInfoIteratorDummy>(qux_doc_hit_infos, "qux");
+    query_term_iterators["lux"] =
+        std::make_unique<DocHitInfoIteratorDummy>(lux_doc_hit_infos, "lux");
+
+    // Score "lux"
+    ScoredDocumentHit lux_expected_scored_doc_hit1(document_id1,
+                                                   kSectionIdMask2,
+                                                   /*score=*/0.692416);
+    ScoredDocumentHit lux_expected_scored_doc_hit2(document_id3,
+                                                   kSectionIdMask2,
+                                                   /*score=*/0.416939);
+    EXPECT_THAT(
+        scoring_processor->Score(std::move(lux_doc_hit_info_iterator),
+                                 /*num_to_score=*/3, &query_term_iterators),
+        ElementsAre(EqualsScoredDocumentHit(lux_expected_scored_doc_hit1),
+                    EqualsScoredDocumentHit(lux_expected_scored_doc_hit2)));
+  }
+}
+
 TEST_P(ScoringProcessorTest,
        ShouldScoreByRelevanceScore_HitTermWithZeroFrequency) {
   DocumentProto document1 =
@@ -617,7 +856,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   std::unordered_map<std::string, std::unique_ptr<DocHitInfoIterator>>
       query_term_iterators;
@@ -687,7 +927,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   std::unordered_map<std::string, std::unique_ptr<DocHitInfoIterator>>
       query_term_iterators;
@@ -762,7 +1003,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   std::unordered_map<std::string, std::unique_ptr<DocHitInfoIterator>>
       query_term_iterators;
@@ -827,7 +1069,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   ScoringSpecProto spec_proto_with_weights =
       CreateScoringSpecForRankingStrategy(
@@ -846,7 +1089,8 @@
           spec_proto_with_weights, default_semantic_metric_type,
           document_store(), schema_store(),
           fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   std::unordered_map<std::string, std::unique_ptr<DocHitInfoIterator>>
       query_term_iterators;
@@ -937,7 +1181,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   std::unordered_map<std::string, std::unique_ptr<DocHitInfoIterator>>
       query_term_iterators;
@@ -1005,7 +1250,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   EXPECT_THAT(scoring_processor->Score(std::move(doc_hit_info_iterator),
                                        /*num_to_score=*/3),
@@ -1071,7 +1317,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   EXPECT_THAT(scoring_processor->Score(std::move(doc_hit_info_iterator),
                                        /*num_to_score=*/3),
@@ -1137,7 +1384,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   EXPECT_THAT(scoring_processor->Score(std::move(doc_hit_info_iterator),
                                        /*num_to_score=*/3),
@@ -1176,7 +1424,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
   EXPECT_THAT(scoring_processor->Score(std::move(doc_hit_info_iterator),
                                        /*num_to_score=*/4),
               ElementsAre(EqualsScoredDocumentHit(scored_document_hit_default),
@@ -1231,7 +1480,8 @@
       ScoringProcessor::Create(
           spec_proto, default_semantic_metric_type, document_store(),
           schema_store(), fake_clock().GetSystemTimeMilliseconds(),
-          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results));
+          /*join_children_fetcher=*/nullptr, &empty_embedding_query_results,
+          feature_flags_.get()));
 
   EXPECT_THAT(scoring_processor->Score(std::move(doc_hit_info_iterator),
                                        /*num_to_score=*/3),
diff --git a/icing/scoring/section-weights_test.cc b/icing/scoring/section-weights_test.cc
index 28b1797..c90ca4a 100644
--- a/icing/scoring/section-weights_test.cc
+++ b/icing/scoring/section-weights_test.cc
@@ -15,15 +15,19 @@
 #include "icing/scoring/section-weights.h"
 
 #include <cfloat>
+#include <memory>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "icing/feature-flags.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/proto/scoring.pb.h"
 #include "icing/proto/term.pb.h"
 #include "icing/schema-builder.h"
+#include "icing/schema/schema-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
+#include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 
 namespace icing {
@@ -39,13 +43,15 @@
         schema_store_dir_(test_dir_ + "/schema_store") {}
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+
     // Creates file directories
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
     filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str());
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, test_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, test_dir_,
+                                           &fake_clock_, feature_flags_.get()));
 
     SchemaTypeConfigProto sender_schema =
         SchemaTypeConfigBuilder()
@@ -100,6 +106,7 @@
   SchemaStore *schema_store() { return schema_store_.get(); }
 
  private:
+  std::unique_ptr<FeatureFlags> feature_flags_;
   const std::string test_dir_;
   const std::string schema_store_dir_;
   Filesystem filesystem_;
diff --git a/icing/store/blob-store.cc b/icing/store/blob-store.cc
index ca943af..cb26761 100644
--- a/icing/store/blob-store.cc
+++ b/icing/store/blob-store.cc
@@ -14,14 +14,18 @@
 
 #include "icing/store/blob-store.h"
 
+#include <fcntl.h>
+
 #include <algorithm>
 #include <array>
+#include <cerrno>
 #include <cstdint>
 #include <iterator>
 #include <limits>
 #include <memory>
 #include <string>
 #include <string_view>
+#include <unordered_map>
 #include <unordered_set>
 #include <utility>
 #include <vector>
@@ -30,11 +34,11 @@
 #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/destructible-directory.h"
+#include "icing/file/constants.h"
 #include "icing/file/filesystem.h"
+#include "icing/file/portable-file-backed-proto-log.h"
+#include "icing/proto/blob.pb.h"
 #include "icing/proto/document.pb.h"
-#include "icing/store/dynamic-trie-key-mapper.h"
-#include "icing/store/key-mapper.h"
 #include "icing/util/clock.h"
 #include "icing/util/encode-util.h"
 #include "icing/util/logging.h"
@@ -44,19 +48,27 @@
 namespace icing {
 namespace lib {
 
-static constexpr std::string_view kKeyMapperDir = "key_mapper";
-// - Key: sha 256 digest (32 bytes)
-// - Value: BlobInfo (24 bytes)
-// Allow max 1M of blob info entries.
-static constexpr int32_t kBlobInfoMapperMaxSize = 56 * 1024 * 1024;  // 56 MiB
+static constexpr std::string_view kBlobFileDir = "blob_files";
+static constexpr std::string_view kBlobInfoProtoLogFileName =
+    "blob_info_proto_file";
 static constexpr int32_t kSha256LengthBytes = 32;
 static constexpr int32_t kReadBufferSize = 8192;
 
-std::string MakeKeyMapperDir(const std::string& base_dir) {
-  return absl_ports::StrCat(base_dir, "/", kKeyMapperDir);
+namespace {
+
+std::string MakeBlobInfoProtoLogFileName(const std::string& base_dir) {
+  return absl_ports::StrCat(base_dir, "/", kBlobInfoProtoLogFileName);
 }
 
-namespace {
+std::string MakeBlobFileDir(const std::string& base_dir) {
+  return absl_ports::StrCat(base_dir, "/", kBlobFileDir);
+}
+
+std::string MakeBlobFileName(const std::string& base_dir,
+                             int64_t creation_time_ms) {
+  return absl_ports::StrCat(MakeBlobFileDir(base_dir), "/",
+                            std::to_string(creation_time_ms));
+}
 
 libtextclassifier3::Status ValidateBlobHandle(
     const PropertyProto::BlobHandleProto& blob_handle) {
@@ -64,26 +76,63 @@
     return absl_ports::InvalidArgumentError(
         "Invalid blob handle. The digest is not sha 256 digest.");
   }
+  if (blob_handle.namespace_().empty()) {
+    return absl_ports::InvalidArgumentError(
+        "Invalid blob handle. The namespace is empty.");
+  }
   return libtextclassifier3::Status::OK;
 }
 
+libtextclassifier3::StatusOr<std::unordered_map<std::string, int32_t>>
+LoadBlobHandleToOffsetMapper(
+    PortableFileBackedProtoLog<BlobInfoProto>* blob_info_log) {
+  std::unordered_map<std::string, int32_t> blob_handle_to_offset;
+  auto itr = blob_info_log->GetIterator();
+  while (itr.Advance().ok()) {
+    auto blob_info_proto_or = blob_info_log->ReadProto(itr.GetOffset());
+    if (!blob_info_proto_or.ok()) {
+      if (absl_ports::IsNotFound(blob_info_proto_or.status())) {
+        // Skip erased proto.
+        continue;
+      }
+
+      // Return real error.
+      return std::move(blob_info_proto_or).status();
+    }
+    BlobInfoProto blob_info_proto = std::move(blob_info_proto_or).ValueOrDie();
+
+    std::string blob_handle_str =
+        BlobStore::BuildBlobHandleStr(blob_info_proto.blob_handle());
+    blob_handle_to_offset.insert({std::move(blob_handle_str), itr.GetOffset()});
+  }
+  return blob_handle_to_offset;
+}
+
 }  // namespace
 
+/* static */ std::string BlobStore::BuildBlobHandleStr(
+    const PropertyProto::BlobHandleProto& blob_handle) {
+  return encode_util::EncodeStringToCString(blob_handle.digest() +
+                                            blob_handle.namespace_());
+}
+
 libtextclassifier3::StatusOr<BlobStore> BlobStore::Create(
     const Filesystem* filesystem, std::string base_dir, const Clock* clock,
-    int64_t orphan_blob_time_to_live_ms) {
+    int64_t orphan_blob_time_to_live_ms, int32_t compression_level) {
   ICING_RETURN_ERROR_IF_NULL(filesystem);
   ICING_RETURN_ERROR_IF_NULL(clock);
-  ICING_ASSIGN_OR_RETURN(
-      std::unique_ptr<KeyMapper<BlobInfo>> blob_info_mapper,
-      DynamicTrieKeyMapper<BlobInfo>::Create(
-          *filesystem, MakeKeyMapperDir(base_dir), kBlobInfoMapperMaxSize));
+
+  // Make sure the blob file directory exists.
+  if (!filesystem->CreateDirectoryRecursively(
+          MakeBlobFileDir(base_dir).c_str())) {
+    return absl_ports::InternalError(
+        absl_ports::StrCat("Could not create blob file directory."));
+  }
 
   // Load existing file names (excluding the directory of key mapper).
   std::vector<std::string> file_names;
-  std::unordered_set<std::string> excludes = {kKeyMapperDir.data()};
-  if (!filesystem->ListDirectory(base_dir.c_str(), excludes,
-                                 /*recursive=*/false, &file_names)) {
+  if (!filesystem->ListDirectory(MakeBlobFileDir(base_dir).c_str(),
+                                 &file_names)) {
     return absl_ports::InternalError("Failed to list directory.");
   }
   std::unordered_set<std::string> known_file_names(
@@ -92,51 +141,111 @@
   if (orphan_blob_time_to_live_ms <= 0) {
     orphan_blob_time_to_live_ms = std::numeric_limits<int64_t>::max();
   }
-  return BlobStore(filesystem, std::move(base_dir), clock,
-                   orphan_blob_time_to_live_ms, std::move(blob_info_mapper),
-                   std::move(known_file_names));
+
+  std::string blob_info_proto_file_name =
+      MakeBlobInfoProtoLogFileName(base_dir);
+
+  ICING_ASSIGN_OR_RETURN(
+      PortableFileBackedProtoLog<BlobInfoProto>::CreateResult log_create_result,
+      PortableFileBackedProtoLog<BlobInfoProto>::Create(
+          filesystem, blob_info_proto_file_name,
+          PortableFileBackedProtoLog<BlobInfoProto>::Options(
+              /*compress_in=*/true, constants::kMaxProtoSize,
+              compression_level)));
+
+  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, std::move(log_create_result.proto_log),
+      std::move(blob_handle_to_offset), std::move(known_file_names));
 }
 
 libtextclassifier3::StatusOr<int> BlobStore::OpenWrite(
     const PropertyProto::BlobHandleProto& blob_handle) {
   ICING_RETURN_IF_ERROR(ValidateBlobHandle(blob_handle));
-  std::string blob_handle_str =
-      encode_util::EncodeStringToCString(blob_handle.digest()) +
-      blob_handle.label();
-  ICING_ASSIGN_OR_RETURN(BlobInfo blob_info,
-                         GetOrCreateBlobInfo(blob_handle_str));
-  if (blob_info.is_committed) {
-    return absl_ports::AlreadyExistsError(
-        "Rewriting the committed blob is not allowed.");
+  std::string blob_handle_str = BuildBlobHandleStr(blob_handle);
+
+  auto blob_info_itr = blob_handle_to_offset_.find(blob_handle_str);
+  if (blob_info_itr != blob_handle_to_offset_.end()) {
+    ICING_ASSIGN_OR_RETURN(BlobInfoProto blob_info,
+                           blob_info_log_->ReadProto(blob_info_itr->second));
+    if (blob_info.is_committed()) {
+      // The blob is already committed, return error.
+      return absl_ports::AlreadyExistsError(absl_ports::StrCat(
+          "Rewriting the committed blob is not allowed for blob handle: ",
+          blob_handle.digest()));
+    }
   }
-  std::string file_name = absl_ports::StrCat(
-      base_dir_, "/", std::to_string(blob_info.creation_time_ms));
+
+  // Create a new blob info and blob file.
+  ICING_ASSIGN_OR_RETURN(BlobInfoProto blob_info,
+                         GetOrCreateBlobInfo(blob_handle_str, blob_handle));
+
+  std::string file_name =
+      MakeBlobFileName(base_dir_, blob_info.creation_time_ms());
   int file_descriptor = filesystem_.OpenForWrite(file_name.c_str());
   if (file_descriptor < 0) {
     return absl_ports::InternalError(absl_ports::StrCat(
-        "Failed to open blob file for handle: ", blob_handle.label()));
+        "Failed to open blob file for handle: ", blob_handle.digest()));
   }
   return file_descriptor;
 }
 
+libtextclassifier3::Status BlobStore::RemoveBlob(
+    const PropertyProto::BlobHandleProto& blob_handle) {
+  ICING_RETURN_IF_ERROR(ValidateBlobHandle(blob_handle));
+  std::string blob_handle_str = BuildBlobHandleStr(blob_handle);
+
+  auto blob_info_itr = blob_handle_to_offset_.find(blob_handle_str);
+  if (blob_info_itr == blob_handle_to_offset_.end()) {
+    return absl_ports::NotFoundError(absl_ports::StrCat(
+        "Cannot find the blob for handle: ", blob_handle.digest()));
+  }
+
+  int64_t blob_info_offset = blob_info_itr->second;
+  ICING_ASSIGN_OR_RETURN(BlobInfoProto blob_info,
+                         blob_info_log_->ReadProto(blob_info_offset));
+
+  std::string file_name =
+      MakeBlobFileName(base_dir_, blob_info.creation_time_ms());
+  if (!filesystem_.DeleteFile(file_name.c_str())) {
+    return absl_ports::InternalError(absl_ports::StrCat(
+        "Failed to abandon blob file for handle: ", blob_handle.digest()));
+  }
+  ICING_RETURN_IF_ERROR(blob_info_log_->EraseProto(blob_info_offset));
+  blob_handle_to_offset_.erase(blob_info_itr);
+
+  has_mutated_ = true;
+  return libtextclassifier3::Status::OK;
+}
+
 libtextclassifier3::StatusOr<int> BlobStore::OpenRead(
     const PropertyProto::BlobHandleProto& blob_handle) {
   ICING_RETURN_IF_ERROR(ValidateBlobHandle(blob_handle));
-  std::string blob_handle_str =
-      encode_util::EncodeStringToCString(blob_handle.digest()) +
-      blob_handle.label();
-  ICING_ASSIGN_OR_RETURN(BlobInfo blob_info,
-                         blob_info_mapper_->Get(blob_handle_str));
-  if (!blob_info.is_committed) {
-    return absl_ports::NotFoundError("Cannot find blob file to read.");
+  std::string blob_handle_str = BuildBlobHandleStr(blob_handle);
+  auto itr = blob_handle_to_offset_.find(blob_handle_str);
+  if (itr == blob_handle_to_offset_.end()) {
+    return absl_ports::NotFoundError(absl_ports::StrCat(
+        "Cannot find the blob for handle: ", blob_handle.digest()));
+  }
+  ICING_ASSIGN_OR_RETURN(BlobInfoProto blob_info,
+                         blob_info_log_->ReadProto(itr->second));
+  if (!blob_info.is_committed()) {
+    // The blob is not committed, return error.
+    return absl_ports::NotFoundError(absl_ports::StrCat(
+        "Cannot find the blob for handle: ", blob_handle.digest()));
   }
 
-  std::string file_name = absl_ports::StrCat(
-      base_dir_, "/", std::to_string(blob_info.creation_time_ms));
+  std::string file_name =
+      MakeBlobFileName(base_dir_, blob_info.creation_time_ms());
   int file_descriptor = filesystem_.OpenForRead(file_name.c_str());
   if (file_descriptor < 0) {
     return absl_ports::InternalError(absl_ports::StrCat(
-        "Failed to open blob file for handle: ", blob_handle.label()));
+        "Failed to open blob file for handle: ", blob_handle.digest()));
   }
   return file_descriptor;
 }
@@ -144,96 +253,137 @@
 libtextclassifier3::Status BlobStore::CommitBlob(
     const PropertyProto::BlobHandleProto& blob_handle) {
   ICING_RETURN_IF_ERROR(ValidateBlobHandle(blob_handle));
-  std::string blob_handle_str =
-      encode_util::EncodeStringToCString(blob_handle.digest()) +
-      blob_handle.label();
-  ICING_ASSIGN_OR_RETURN(BlobInfo blob_info,
-                         blob_info_mapper_->Get(blob_handle_str));
-  if (blob_info.is_committed) {
+
+  std::string blob_handle_str = BuildBlobHandleStr(blob_handle);
+
+  auto pending_blob_info_itr = blob_handle_to_offset_.find(blob_handle_str);
+  if (pending_blob_info_itr == blob_handle_to_offset_.end()) {
+    return absl_ports::NotFoundError(absl_ports::StrCat(
+        "Cannot find the blob for handle: ", blob_handle.digest()));
+  }
+  int64_t pending_blob_info_offset = pending_blob_info_itr->second;
+
+  ICING_ASSIGN_OR_RETURN(BlobInfoProto blob_info_proto,
+                         blob_info_log_->ReadProto(pending_blob_info_offset));
+
+  // Check if the blob is already committed.
+  if (blob_info_proto.is_committed()) {
     return absl_ports::AlreadyExistsError(absl_ports::StrCat(
-        "The blob is already committed for handle: ", blob_handle.label()));
+        "The blob is already committed for handle: ", blob_handle.digest()));
   }
 
   // Read the file and verify the digest.
-  std::string file_name = absl_ports::StrCat(
-      base_dir_, "/", std::to_string(blob_info.creation_time_ms));
-  ScopedFd sfd(filesystem_.OpenForRead(file_name.c_str()));
-  if (!sfd.is_valid()) {
-    return absl_ports::InternalError(absl_ports::StrCat(
-        "Failed to open blob file for handle: ", blob_handle.label()));
-  }
 
-  int64_t file_size = filesystem_.GetFileSize(sfd.get());
-  if (file_size == Filesystem::kBadFileSize) {
-    return absl_ports::InternalError(absl_ports::StrCat(
-        "Failed to get file size for handle: ", blob_handle.label()));
-  }
-
+  std::string file_name =
+      MakeBlobFileName(base_dir_, blob_info_proto.creation_time_ms());
   Sha256 sha256;
-  // Read 8 KiB per iteration
-  int64_t prev_total_read_size = 0;
-  auto buffer = std::make_unique<uint8_t[]>(kReadBufferSize);
-  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.get(), size_to_read)) {
+  {
+    ScopedFd sfd(filesystem_.OpenForRead(file_name.c_str()));
+    if (!sfd.is_valid()) {
       return absl_ports::InternalError(absl_ports::StrCat(
-          "Failed to read blob file for handle: ", blob_handle.label()));
+          "Failed to open blob file for handle: ", blob_handle.digest()));
     }
 
-    sha256.Update(buffer.get(), size_to_read);
-    prev_total_read_size += size_to_read;
-  }
-  std::array<uint8_t, 32> hash = std::move(sha256).Finalize();
+    int64_t file_size = filesystem_.GetFileSize(sfd.get());
+    if (file_size == Filesystem::kBadFileSize) {
+      return absl_ports::InternalError(absl_ports::StrCat(
+          "Failed to get file size for handle: ", blob_handle.digest()));
+    }
 
+    // Read 8 KiB per iteration
+    int64_t prev_total_read_size = 0;
+    uint8_t buffer[kReadBufferSize];
+    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)) {
+        return absl_ports::InternalError(absl_ports::StrCat(
+            "Failed to read blob file for handle: ", blob_handle.digest()));
+      }
+
+      sha256.Update(buffer, size_to_read);
+      prev_total_read_size += size_to_read;
+    }
+  }
+
+  std::array<uint8_t, 32> hash = std::move(sha256).Finalize();
   const std::string& digest = blob_handle.digest();
+
+  // Close active file descriptor and cached pending blob info for the pending
+  // blob handle before we verify the digest. This is needed anyway. We will add
+  // cached pending blob info back if the digest is valid.
+
+  ICING_RETURN_IF_ERROR(blob_info_log_->EraseProto(pending_blob_info_offset));
+
   if (digest.length() != hash.size() ||
       digest.compare(0, digest.length(),
                      reinterpret_cast<const char*>(hash.data()),
                      hash.size()) != 0) {
+    // The blob content doesn't match to the digest. Delete this corrupted blob.
+    if (!filesystem_.DeleteFile(file_name.c_str())) {
+      return absl_ports::InternalError(absl_ports::StrCat(
+          "Failed to delete corrupted blob file for handle: ",
+          blob_handle.digest()));
+    }
     return absl_ports::InvalidArgumentError(
         "The blob content doesn't match to the digest.");
   }
 
-  // Mark the blob is committed
-  blob_info.is_committed = true;
   has_mutated_ = true;
-  ICING_RETURN_IF_ERROR(blob_info_mapper_->Put(blob_handle_str, blob_info));
+  blob_info_proto.set_is_committed(true);
+  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 blob_info_offset_or.status();
+  }
+  blob_handle_to_offset_[blob_handle_str] = blob_info_offset_or.ValueOrDie();
 
   return libtextclassifier3::Status::OK;
 }
 
 libtextclassifier3::Status BlobStore::PersistToDisk() {
   if (has_mutated_) {
-    ICING_RETURN_IF_ERROR(blob_info_mapper_->PersistToDisk());
+    ICING_RETURN_IF_ERROR(blob_info_log_->PersistToDisk());
     has_mutated_ = false;
   }
   return libtextclassifier3::Status::OK;
 }
 
-libtextclassifier3::StatusOr<BlobStore::BlobInfo>
-BlobStore::GetOrCreateBlobInfo(const std::string& blob_handle_str) {
-  libtextclassifier3::StatusOr<BlobInfo> blob_info_or =
-      blob_info_mapper_->Get(blob_handle_str);
-
-  if (absl_ports::IsNotFound(blob_info_or.status())) {
-    // Create a new blob info, we are using creation time as the unique file
-    // name.
-    int64_t timestamp = clock_.GetSystemTimeMilliseconds();
-    std::string file_name = std::to_string(timestamp);
-    while (known_file_names_.find(file_name) != known_file_names_.end()) {
-      ++timestamp;
-      file_name = std::to_string(timestamp);
-    }
-    known_file_names_.insert(file_name);
-
-    BlobInfo blob_info = {timestamp, /*is_committed=*/false};
-    ICING_RETURN_IF_ERROR(blob_info_mapper_->Put(blob_handle_str, blob_info));
-    has_mutated_ = true;
-    return blob_info;
+libtextclassifier3::StatusOr<BlobInfoProto> BlobStore::GetOrCreateBlobInfo(
+    const std::string& blob_handle_str,
+    const PropertyProto::BlobHandleProto& blob_handle) {
+  auto itr = blob_handle_to_offset_.find(blob_handle_str);
+  if (itr != blob_handle_to_offset_.end()) {
+    return blob_info_log_->ReadProto(itr->second);
   }
 
-  return blob_info_or;
+  // Create a new blob info, we are using creation time as the unique file
+  // name.
+  int64_t timestamp = clock_.GetSystemTimeMilliseconds();
+  std::string file_name = std::to_string(timestamp);
+  while (known_file_names_.find(file_name) != known_file_names_.end()) {
+    ++timestamp;
+    file_name = std::to_string(timestamp);
+  }
+  known_file_names_.insert(file_name);
+
+  BlobInfoProto blob_info_proto;
+  *blob_info_proto.mutable_blob_handle() = blob_handle;
+  blob_info_proto.set_creation_time_ms(timestamp);
+  blob_info_proto.set_is_committed(false);
+
+  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 blob_info_offset_or.status();
+  }
+
+  has_mutated_ = true;
+  blob_handle_to_offset_[blob_handle_str] = blob_info_offset_or.ValueOrDie();
+
+  return blob_info_proto;
 }
 
 std::unordered_set<std::string>
@@ -243,15 +393,18 @@
     // Nothing to optimize, return empty set.
     return std::unordered_set<std::string>();
   }
-  int64_t expired_threshold =
-      clock_.GetSystemTimeMilliseconds() - orphan_blob_time_to_live_ms_;
-  std::unique_ptr<typename KeyMapper<BlobInfo>::Iterator> itr =
-      blob_info_mapper_->GetIterator();
-
+  int64_t expired_threshold = current_time_ms - orphan_blob_time_to_live_ms_;
   std::unordered_set<std::string> expired_blob_handles;
-  while (itr->Advance()) {
-    if (itr->GetValue().creation_time_ms < expired_threshold) {
-      expired_blob_handles.insert(std::string(itr->GetKey()));
+  auto itr = blob_info_log_->GetIterator();
+  while (itr.Advance().ok()) {
+    auto blob_info_proto_or = blob_info_log_->ReadProto(itr.GetOffset());
+    if (!blob_info_proto_or.ok()) {
+      continue;
+    }
+    BlobInfoProto blob_info_proto = std::move(blob_info_proto_or).ValueOrDie();
+    if (blob_info_proto.creation_time_ms() < expired_threshold) {
+      expired_blob_handles.insert(
+          BuildBlobHandleStr(blob_info_proto.blob_handle()));
     }
   }
   return expired_blob_handles;
@@ -259,64 +412,133 @@
 
 libtextclassifier3::Status BlobStore::Optimize(
     const std::unordered_set<std::string>& dead_blob_handles) {
-  if (dead_blob_handles.empty()) {
-    // nothing to optimize, return early.
-    return libtextclassifier3::Status::OK;
+  // Create the temp blob info log file.
+  std::string temp_blob_info_proto_file_name =
+      absl_ports::StrCat(MakeBlobInfoProtoLogFileName(base_dir_), "_temp");
+  if (!filesystem_.DeleteFile(temp_blob_info_proto_file_name.c_str())) {
+    return absl_ports::InternalError(
+        "Unable to delete temp file to prepare to build new blob proto file.");
   }
 
-  // Create the temp blob store directory.
-  std::string temp_blob_store_dir_path = base_dir_ + "_temp";
-  if (!filesystem_.DeleteDirectoryRecursively(
-          temp_blob_store_dir_path.c_str())) {
-    ICING_LOG(ERROR) << "Recursively deleting "
-                     << temp_blob_store_dir_path.c_str();
-    return absl_ports::InternalError(
-        "Unable to delete temp directory to prepare to build new blob store.");
-  }
-  DestructibleDirectory temp_blob_store_dir(&filesystem_,
-                                            temp_blob_store_dir_path);
-  if (!temp_blob_store_dir.is_valid()) {
-    return absl_ports::InternalError(
-        "Unable to create temp directory to build new blob store.");
-  }
+  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_)));
+  std::unique_ptr<PortableFileBackedProtoLog<BlobInfoProto>> new_blob_info_log =
+      std::move(temp_log_create_result.proto_log);
 
-  // Destroy the old blob info mapper and replace it with the new one.
-  std::string new_key_mapper_dir = MakeKeyMapperDir(temp_blob_store_dir_path);
-  ICING_ASSIGN_OR_RETURN(
-      std::unique_ptr<KeyMapper<BlobInfo>> new_blob_info_mapper,
-      DynamicTrieKeyMapper<BlobInfo>::Create(filesystem_, new_key_mapper_dir,
-                                             kBlobInfoMapperMaxSize));
-  std::unique_ptr<typename KeyMapper<BlobInfo>::Iterator> itr =
-      blob_info_mapper_->GetIterator();
-  while (itr->Advance()) {
-    if (dead_blob_handles.find(std::string(itr->GetKey())) ==
-        dead_blob_handles.end()) {
-      ICING_RETURN_IF_ERROR(
-          new_blob_info_mapper->Put(itr->GetKey(), itr->GetValue()));
-    } else {
-      // Delete the file of dead blobs.
-      std::string file_name = absl_ports::StrCat(
-          base_dir_, "/", std::to_string(itr->GetValue().creation_time_ms));
+  auto itr = blob_info_log_->GetIterator();
+  std::unordered_map<std::string, int32_t> new_blob_handle_to_offset;
+  while (itr.Advance().ok()) {
+    auto blob_info_proto_or = blob_info_log_->ReadProto(itr.GetOffset());
+    if (!blob_info_proto_or.ok()) {
+      if (absl_ports::IsNotFound(blob_info_proto_or.status())) {
+        // Skip erased proto.
+        continue;
+      }
+
+      // Return real error.
+      return std::move(blob_info_proto_or).status();
+    }
+    BlobInfoProto blob_info_proto = std::move(blob_info_proto_or).ValueOrDie();
+    std::string blob_handle_str =
+        BuildBlobHandleStr(blob_info_proto.blob_handle());
+    if (dead_blob_handles.find(blob_handle_str) != dead_blob_handles.end()) {
+      // Delete all dead blob files.
+
+      std::string file_name =
+          MakeBlobFileName(base_dir_, blob_info_proto.creation_time_ms());
       if (!filesystem_.DeleteFile(file_name.c_str())) {
         return absl_ports::InternalError(
             absl_ports::StrCat("Failed to delete blob file: ", file_name));
       }
+    } else {
+      // Write the alive blob info to the new blob info log file.
+      ICING_ASSIGN_OR_RETURN(int32_t new_offset,
+                             new_blob_info_log->WriteProto(blob_info_proto));
+      new_blob_handle_to_offset[blob_handle_str] = new_offset;
     }
   }
-  new_blob_info_mapper.reset();
+  new_blob_info_log->PersistToDisk();
+  new_blob_info_log.reset();
+  blob_info_log_.reset();
+  std::string old_blob_info_proto_file_name =
+      MakeBlobInfoProtoLogFileName(base_dir_);
   // Then we swap the new key mapper directory with the old one.
-  if (!filesystem_.SwapFiles(MakeKeyMapperDir(base_dir_).c_str(),
-                             new_key_mapper_dir.c_str())) {
+  if (!filesystem_.SwapFiles(old_blob_info_proto_file_name.c_str(),
+                             temp_blob_info_proto_file_name.c_str())) {
     return absl_ports::InternalError(
         "Unable to apply new blob store due to failed swap!");
   }
-  ICING_ASSIGN_OR_RETURN(
-      blob_info_mapper_,
-      DynamicTrieKeyMapper<BlobInfo>::Create(
-          filesystem_, MakeKeyMapperDir(base_dir_), kBlobInfoMapperMaxSize));
 
+  // Delete the temp file, don't need to throw error if it fails, it will be
+  // deleted in the next run.
+  filesystem_.DeleteFile(temp_blob_info_proto_file_name.c_str());
+
+  ICING_ASSIGN_OR_RETURN(
+      PortableFileBackedProtoLog<BlobInfoProto>::CreateResult log_create_result,
+      PortableFileBackedProtoLog<BlobInfoProto>::Create(
+          &filesystem_, old_blob_info_proto_file_name,
+          PortableFileBackedProtoLog<BlobInfoProto>::Options(
+              /*compress_in=*/true, constants::kMaxProtoSize,
+              compression_level_)));
+  blob_info_log_ = std::move(log_create_result.proto_log);
+  blob_handle_to_offset_ = std::move(new_blob_handle_to_offset);
   return libtextclassifier3::Status::OK;
 }
 
+libtextclassifier3::StatusOr<std::vector<NamespaceBlobStorageInfoProto>>
+BlobStore::GetStorageInfo() const {
+  // Get the file size of each namespace offset.
+  std::unordered_map<std::string, NamespaceBlobStorageInfoProto>
+      namespace_to_storage_info;
+  auto itr = blob_info_log_->GetIterator();
+  while (itr.Advance().ok()) {
+    auto blob_info_proto_or = blob_info_log_->ReadProto(itr.GetOffset());
+    if (!blob_info_proto_or.ok()) {
+      if (absl_ports::IsNotFound(blob_info_proto_or.status())) {
+        // Skip erased proto.
+        continue;
+      }
+
+      // Return real error.
+      return std::move(blob_info_proto_or).status();
+    }
+    BlobInfoProto blob_info_proto = std::move(blob_info_proto_or).ValueOrDie();
+
+    std::string file_name =
+        MakeBlobFileName(base_dir_, blob_info_proto.creation_time_ms());
+    int64_t file_size = filesystem_.GetFileSize(file_name.c_str());
+    if (file_size == Filesystem::kBadFileSize) {
+      ICING_LOG(WARNING) << "Bad file size for blob file: " << file_name;
+      continue;
+    }
+    std::string name_space = blob_info_proto.blob_handle().namespace_();
+    NamespaceBlobStorageInfoProto namespace_blob_storage_info =
+        namespace_to_storage_info[name_space];
+    namespace_blob_storage_info.set_namespace_(name_space);
+    namespace_blob_storage_info.set_blob_size(
+        namespace_blob_storage_info.blob_size() + file_size);
+    namespace_blob_storage_info.set_num_blobs(
+        namespace_blob_storage_info.num_blobs() + 1);
+    namespace_to_storage_info[name_space] =
+        std::move(namespace_blob_storage_info);
+  }
+
+  // Create the namespace blob storage info for each namespace.
+  std::vector<NamespaceBlobStorageInfoProto> namespace_blob_storage_infos;
+  namespace_blob_storage_infos.reserve(namespace_to_storage_info.size());
+  for (const auto& [_, namespace_blob_storage_info] :
+       namespace_to_storage_info) {
+    namespace_blob_storage_infos.push_back(
+        std::move(namespace_blob_storage_info));
+  }
+
+  return namespace_blob_storage_infos;
+}
+
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/store/blob-store.h b/icing/store/blob-store.h
index 821f896..fb1a773 100644
--- a/icing/store/blob-store.h
+++ b/icing/store/blob-store.h
@@ -16,17 +16,18 @@
 #define ICING_STORE_BLOB_STORE_H_
 
 #include <cstdint>
-#include <memory>
 #include <string>
-#include <string_view>
+#include <unordered_map>
 #include <unordered_set>
 #include <utility>
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/file/filesystem.h"
+#include "icing/file/portable-file-backed-proto-log.h"
+#include "icing/proto/blob.pb.h"
 #include "icing/proto/document.pb.h"
-#include "icing/store/key-mapper.h"
+#include "icing/proto/storage.pb.h"
 #include "icing/util/clock.h"
 
 namespace icing {
@@ -47,25 +48,10 @@
 // The BlobStore is not thread-safe.
 class BlobStore {
  public:
-  // BlobInfo holds information about a blob. This struct will be stored as the
-  // value in the dynamic trie key mapper, so it must be packed to avoid
-  // padding (which potentially causes use-of-uninitialized-value errors).
-  struct BlobInfo {
-    // The creation time of the blob. This is used to determine when to delete
-    // the orphaned blobs.
-    // We are using creation_time_ms to be file name of the blob, so this field
-    // is unique for each blob.
-    int64_t creation_time_ms;
-    bool is_committed;
-
-    // The param needed for dynamic trie, we shouldn't call this constructor
-    // directly.
-    BlobInfo() : BlobInfo(/*creation_time_ms=*/-1, /*is_committed=*/false) {}
-
-    BlobInfo(int64_t creation_time_ms, bool is_committed)
-        : creation_time_ms(creation_time_ms), is_committed(is_committed) {}
-  } __attribute__((packed));
-  static_assert(sizeof(BlobInfo) == 9, "Invalid BlobInfo size");
+  // Builds a string representation of a blob handle.
+  // The string is used as the key in the key mapper.
+  static std::string BuildBlobHandleStr(
+      const PropertyProto::BlobHandleProto& blob_handle);
 
   // Factory function to create a BlobStore instance. The base directory is
   // used to persist blobs. If a blob store was previously created with
@@ -75,31 +61,50 @@
   //
   // Returns:
   //   A BlobStore on success
-  //   FAILED_PRECONDITION on any null pointer input
+  //   FAILED_PRECONDITION_ERROR on any null pointer input
   //   INTERNAL_ERROR on I/O error
   static libtextclassifier3::StatusOr<BlobStore> Create(
       const Filesystem* filesystem, std::string base_dir, const Clock* clock,
-      int64_t orphan_blob_time_to_live_ms);
+      int64_t orphan_blob_time_to_live_ms, int32_t compression_level);
 
   // 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.
   //
+  // It is the user's responsibility to close the file descriptor after writing
+  // is done and should operate on the file descriptor after commit or remove
+  // it.
+  //
   // Returns:
   //   File descriptor (writable) on success
-  //   INVALID_ARGUMENT on invalid blob handle
-  //   ALREADY_EXISTS if the blob has already been committed
+  //   INVALID_ARGUMENT_ERROR on invalid blob handle
+  //   FAILED_PRECONDITION_ERROR on blob is already opened for write
+  //   ALREADY_EXISTS_ERROR if the blob has already been committed
   //   INTERNAL_ERROR on IO error
   libtextclassifier3::StatusOr<int> OpenWrite(
       const PropertyProto::BlobHandleProto& blob_handle);
 
+  // Removes a blob file and blob handle from the blob store.
+  //
+  // This will remove the blob on any state. No matter it's committed or not or
+  // it has reference document links or not.
+  //
+  // Returns:
+  //   INVALID_ARGUMENT_ERROR on invalid blob handle
+  //   NOT_FOUND_ERROR on blob is not found
+  //   INTERNAL_ERROR on IO error
+  libtextclassifier3::Status RemoveBlob(
+      const PropertyProto::BlobHandleProto& blob_handle);
+
   // Gets a file for read only purpose for the given blob handle.
   // Will only succeed for blobs that were committed by calling CommitBlob.
   //
+  // It is the user's responsibility to close the file descriptor after reading.
+  //
   // Returns:
   //   File descriptor (read only) on success
-  //   INVALID_ARGUMENT on invalid blob handle
-  //   NOT_FOUND on blob is not found or is not committed
+  //   INVALID_ARGUMENT_ERROR on invalid blob handle
+  //   NOT_FOUND_ERROR on blob is not found or is not committed
   libtextclassifier3::StatusOr<int> OpenRead(
       const PropertyProto::BlobHandleProto& blob_handle);
 
@@ -110,10 +115,10 @@
   //
   // Returns:
   //   OK on the blob is successfully committed.
-  //   ALREADY_EXISTS on the blob is already committed, this is no op.
-  //   INVALID_ARGUMENT on invalid blob handle or digest is mismatch with
+  //   ALREADY_EXISTS_ERROR on the blob is already committed, this is no op.
+  //   INVALID_ARGUMENT_ERROR on invalid blob handle or digest is mismatch with
   //                        file content.
-  //   NOT_FOUND on blob is not found.
+  //   NOT_FOUND_ERROR on blob is not found.
   libtextclassifier3::Status CommitBlob(
       const PropertyProto::BlobHandleProto& blob_handle);
 
@@ -140,28 +145,52 @@
   libtextclassifier3::Status Optimize(
       const std::unordered_set<std::string>& dead_blob_handles);
 
- private:
-  explicit BlobStore(const Filesystem* filesystem, std::string base_dir,
-                     const Clock* clock, int64_t orphan_blob_time_to_live_ms,
-                     std::unique_ptr<KeyMapper<BlobInfo>> blob_info_mapper,
-                     std::unordered_set<std::string> known_file_names)
+  // Calculates the StorageInfo for the Blob Store.
+  //
+  // Returns:
+  //   Vector of NamespaceBlobStorageInfoProto contains size of each namespace.
+  //   INTERNAL_ERROR on I/O error
+  libtextclassifier3::StatusOr<std::vector<NamespaceBlobStorageInfoProto>>
+  GetStorageInfo() const;
+
+private:
+  explicit BlobStore(
+      const Filesystem* filesystem, std::string base_dir, const Clock* clock,
+      int64_t orphan_blob_time_to_live_ms, int32_t compression_level,
+      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)
       : filesystem_(*filesystem),
         base_dir_(std::move(base_dir)),
         clock_(*clock),
         orphan_blob_time_to_live_ms_(orphan_blob_time_to_live_ms),
-        blob_info_mapper_(std::move(blob_info_mapper)),
+        compression_level_(compression_level),
+        blob_info_log_(std::move(blob_info_log)),
+        blob_handle_to_offset_(std::move(blob_handle_to_offset)),
         known_file_names_(std::move(known_file_names)) {}
 
-  libtextclassifier3::StatusOr<BlobStore::BlobInfo> GetOrCreateBlobInfo(
-      const std::string& blob_handle_str);
+  libtextclassifier3::StatusOr<BlobInfoProto> GetOrCreateBlobInfo(
+      const std::string& blob_handle_str,
+      const PropertyProto::BlobHandleProto& blob_handle);
 
   const Filesystem& filesystem_;
   std::string base_dir_;
   const Clock& clock_;
   int64_t orphan_blob_time_to_live_ms_;
+  int32_t compression_level_;
 
-  std::unique_ptr<KeyMapper<BlobInfo>> blob_info_mapper_;
+  // The ground truth blob info log file, which is used to read/write/erase
+  // BlobInfoProto.
+  std::unique_ptr<PortableFileBackedProtoLog<BlobInfoProto>> blob_info_log_;
+
+  // The map for BlobHandle string to the offset of BlobInfoProto in the
+  // BlobInfoProto log file.
+  // The keys are the Encoded CString from BlobHandleProto.
+  std::unordered_map<std::string, int32_t> blob_handle_to_offset_;
+
+  // The set of used file names to store blobs in the blob store.
   std::unordered_set<std::string> known_file_names_;
+
   bool has_mutated_ = false;
 };
 
diff --git a/icing/store/document-associated-score-data.h b/icing/store/document-associated-score-data.h
index 9a711c8..c7ee663 100644
--- a/icing/store/document-associated-score-data.h
+++ b/icing/store/document-associated-score-data.h
@@ -33,21 +33,27 @@
 // 3. Document creation timestamp. Unix timestamp of when the document is
 //    created and inserted into Icing.
 // 4. Document length in number of tokens.
+// 5. Index of the ScorablePropertySetProto data of a document in the scorable
+//    property cache, which is owned by the document-store.
 class DocumentAssociatedScoreData {
  public:
   explicit DocumentAssociatedScoreData(CorpusId corpus_id, int document_score,
                                        int64_t creation_timestamp_ms,
+                                       int32_t scorable_property_cache_index,
                                        int length_in_tokens = 0)
       : creation_timestamp_ms_(creation_timestamp_ms),
         corpus_id_(corpus_id),
         document_score_(document_score),
-        length_in_tokens_(length_in_tokens) {}
+        length_in_tokens_(length_in_tokens),
+        scorable_property_cache_index_(scorable_property_cache_index) {}
 
   bool operator==(const DocumentAssociatedScoreData& other) const {
     return document_score_ == other.document_score() &&
            creation_timestamp_ms_ == other.creation_timestamp_ms() &&
            length_in_tokens_ == other.length_in_tokens() &&
-           corpus_id_ == other.corpus_id();
+           corpus_id_ == other.corpus_id() &&
+           scorable_property_cache_index_ ==
+               other.scorable_property_cache_index();
   }
 
   CorpusId corpus_id() const { return corpus_id_; }
@@ -58,15 +64,25 @@
 
   int length_in_tokens() const { return length_in_tokens_; }
 
+  int32_t scorable_property_cache_index() const {
+    return scorable_property_cache_index_;
+  }
+
+  void set_scorable_property_cache_index(
+      int32_t scorable_property_cache_index) {
+    scorable_property_cache_index_ = scorable_property_cache_index;
+  }
+
  private:
   int64_t creation_timestamp_ms_;
   CorpusId corpus_id_;
   int document_score_;
   int length_in_tokens_;
+  int32_t scorable_property_cache_index_;
 } __attribute__((packed));
 
-static_assert(sizeof(DocumentAssociatedScoreData) == 20,
-              "Size of DocumentAssociatedScoreData should be 20");
+static_assert(sizeof(DocumentAssociatedScoreData) == 24,
+              "Size of DocumentAssociatedScoreData should be 24");
 static_assert(icing_is_packed_pod<DocumentAssociatedScoreData>::value,
               "go/icing-ubsan");
 
diff --git a/icing/store/document-filter-data.h b/icing/store/document-filter-data.h
index 3970132..471ac60 100644
--- a/icing/store/document-filter-data.h
+++ b/icing/store/document-filter-data.h
@@ -30,20 +30,25 @@
 class DocumentFilterData {
  public:
   explicit DocumentFilterData(NamespaceId namespace_id,
+                              uint64_t uri_fingerprint,
                               SchemaTypeId schema_type_id,
                               int64_t expiration_timestamp_ms)
       : expiration_timestamp_ms_(expiration_timestamp_ms),
+        uri_fingerprint_(uri_fingerprint),
         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();
   }
 
   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;
@@ -53,11 +58,12 @@
 
  private:
   int64_t expiration_timestamp_ms_;
+  uint64_t uri_fingerprint_;
   NamespaceId namespace_id_;
   SchemaTypeId schema_type_id_;
 } __attribute__((packed));
 
-static_assert(sizeof(DocumentFilterData) == 12, "");
+static_assert(sizeof(DocumentFilterData) == 20, "");
 static_assert(icing_is_packed_pod<DocumentFilterData>::value, "go/icing-ubsan");
 
 }  // namespace lib
diff --git a/icing/store/document-store.cc b/icing/store/document-store.cc
index e5ed547..449b005 100644
--- a/icing/store/document-store.cc
+++ b/icing/store/document-store.cc
@@ -27,19 +27,21 @@
 
 #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 "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/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"
 #include "icing/file/memory-mapped-file.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/legacy/core/icing-string-util.h"
 #include "icing/proto/debug.pb.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/document_wrapper.pb.h"
+#include "icing/proto/internal/scorable_property_set.pb.h"
 #include "icing/proto/logging.pb.h"
 #include "icing/proto/optimize.pb.h"
 #include "icing/proto/persist.pb.h"
@@ -48,6 +50,8 @@
 #include "icing/proto/usage.pb.h"
 #include "icing/schema/property-util.h"
 #include "icing/schema/schema-store.h"
+#include "icing/schema/scorable_property_manager.h"
+#include "icing/store/blob-store.h"
 #include "icing/store/corpus-associated-scoring-data.h"
 #include "icing/store/corpus-id.h"
 #include "icing/store/document-associated-score-data.h"
@@ -56,7 +60,7 @@
 #include "icing/store/document-log-creator.h"
 #include "icing/store/dynamic-trie-key-mapper.h"
 #include "icing/store/key-mapper.h"
-#include "icing/store/namespace-fingerprint-identifier.h"
+#include "icing/store/namespace-id-fingerprint.h"
 #include "icing/store/namespace-id.h"
 #include "icing/store/persistent-hash-map-key-mapper.h"
 #include "icing/store/usage-store.h"
@@ -64,9 +68,9 @@
 #include "icing/util/clock.h"
 #include "icing/util/crc32.h"
 #include "icing/util/data-loss.h"
-#include "icing/util/encode-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/tokenized-document.h"
 
@@ -77,10 +81,12 @@
 
 // Used in DocumentId mapper to mark a document as deleted
 constexpr int64_t kDocDeletedFlag = -1;
+constexpr int32_t kInvalidScorablePropertyCacheIndex = -1;
 constexpr char kDocumentIdMapperFilename[] = "document_id_mapper";
 constexpr char kUriHashMapperWorkingPath[] = "uri_mapper";
 constexpr char kDocumentStoreHeaderFilename[] = "document_store_header";
 constexpr char kScoreCacheFilename[] = "score_cache";
+constexpr char kScorablePropertyCacheFilename[] = "scorable_property_cache";
 constexpr char kCorpusScoreCache[] = "corpus_score_cache";
 constexpr char kFilterCacheFilename[] = "filter_cache";
 constexpr char kNamespaceMapperFilename[] = "namespace_mapper";
@@ -126,6 +132,10 @@
   return absl_ports::StrCat(base_dir, "/", kScoreCacheFilename);
 }
 
+std::string MakeScorablePropertyCacheFilename(const std::string& base_dir) {
+  return absl_ports::StrCat(base_dir, "/", kScorablePropertyCacheFilename);
+}
+
 std::string MakeCorpusScoreCache(const std::string& base_dir) {
   return absl_ports::StrCat(base_dir, "/", kCorpusScoreCache);
 }
@@ -244,6 +254,10 @@
 // Find the existing blob handles in the given document and remove them from the
 // dead_blob_handles set. Those are the blob handles that are still in use.
 //
+// This method is flag-guarded by the flag enable_blob_store. If the flag is
+// disabled, the dead_blob_handles must be empty and this method will be a
+// no-op.
+//
 // The type_blob_map is a map from schema type to a set of blob property names.
 void RemoveAliveBlobHandles(
     const DocumentProto& document,
@@ -265,9 +279,7 @@
     if (content_or.ok()) {
       for (const PropertyProto::BlobHandleProto& blob_handle :
            content_or.ValueOrDie()) {
-        dead_blob_handles.erase(
-            encode_util::EncodeStringToCString(blob_handle.digest()) +
-            blob_handle.label());
+        dead_blob_handles.erase(BlobStore::BuildBlobHandleStr(blob_handle));
       }
     }
   }
@@ -275,35 +287,19 @@
 
 }  // namespace
 
-std::string DocumentStore::MakeFingerprint(
-    NamespaceId namespace_id, std::string_view namespace_,
-    std::string_view uri_or_schema) const {
-  if (!namespace_id_fingerprint_) {
-    // Using a 64-bit fingerprint to represent the key could lead to collisions.
-    // But, even with 200K unique keys, the probability of collision is about
-    // one-in-a-billion (https://en.wikipedia.org/wiki/Birthday_attack).
-    uint64_t fprint = tc3farmhash::Fingerprint64(
-        absl_ports::StrCat(namespace_, uri_or_schema));
-    return fingerprint_util::GetFingerprintString(fprint);
-  } else {
-    return NamespaceFingerprintIdentifier(namespace_id, uri_or_schema)
-        .EncodeToCString();
-  }
-}
-
 DocumentStore::DocumentStore(const Filesystem* filesystem,
                              const std::string_view base_dir,
                              const Clock* clock,
                              const SchemaStore* schema_store,
-                             bool namespace_id_fingerprint,
+                             const FeatureFlags* feature_flags,
                              bool pre_mapping_fbv, bool use_persistent_hash_map,
                              int32_t compression_level)
     : filesystem_(filesystem),
       base_dir_(base_dir),
       clock_(*clock),
+      feature_flags_(*feature_flags),
       schema_store_(schema_store),
       document_validator_(schema_store),
-      namespace_id_fingerprint_(namespace_id_fingerprint),
       pre_mapping_fbv_(pre_mapping_fbv),
       use_persistent_hash_map_(use_persistent_hash_map),
       compression_level_(compression_level) {}
@@ -333,16 +329,18 @@
 libtextclassifier3::StatusOr<DocumentStore::CreateResult> DocumentStore::Create(
     const Filesystem* filesystem, const std::string& base_dir,
     const Clock* clock, const SchemaStore* schema_store,
-    bool force_recovery_and_revalidate_documents, bool namespace_id_fingerprint,
-    bool pre_mapping_fbv, bool use_persistent_hash_map,
-    int32_t compression_level, InitializeStatsProto* initialize_stats) {
+    const FeatureFlags* feature_flags,
+    bool force_recovery_and_revalidate_documents, bool pre_mapping_fbv,
+    bool use_persistent_hash_map, int32_t compression_level,
+    InitializeStatsProto* initialize_stats) {
   ICING_RETURN_ERROR_IF_NULL(filesystem);
   ICING_RETURN_ERROR_IF_NULL(clock);
   ICING_RETURN_ERROR_IF_NULL(schema_store);
+  ICING_RETURN_ERROR_IF_NULL(feature_flags);
 
   auto document_store = std::unique_ptr<DocumentStore>(new DocumentStore(
-      filesystem, base_dir, clock, schema_store, namespace_id_fingerprint,
-      pre_mapping_fbv, use_persistent_hash_map, compression_level));
+      filesystem, base_dir, clock, schema_store, feature_flags, pre_mapping_fbv,
+      use_persistent_hash_map, compression_level));
   ICING_ASSIGN_OR_RETURN(
       InitializeResult initialize_result,
       document_store->Initialize(force_recovery_and_revalidate_documents,
@@ -395,6 +393,11 @@
   ICING_RETURN_IF_ERROR(FileBackedVector<CorpusAssociatedScoreData>::Delete(
       *filesystem, MakeCorpusScoreCache(base_dir)));
 
+  // Scorable Property Cache
+  ICING_RETURN_IF_ERROR(
+      MemoryMappedFileBackedProtoLog<ScorablePropertySetProto>::Delete(
+          *filesystem, MakeScorablePropertyCacheFilename(base_dir)));
+
   return libtextclassifier3::Status::OK;
 }
 
@@ -502,8 +505,7 @@
         absl_ports::StrCat("Couldn't read: ", MakeHeaderFilename(base_dir_)));
   }
 
-  if (header.magic !=
-      DocumentStore::Header::GetCurrentMagic(namespace_id_fingerprint_)) {
+  if (header.magic != DocumentStore::Header::kMagic) {
     return absl_ports::InternalError(absl_ports::StrCat(
         "Invalid header kMagic for file: ", MakeHeaderFilename(base_dir_)));
   }
@@ -536,6 +538,11 @@
                              *filesystem_, MakeScoreCacheFilename(base_dir_),
                              MemoryMappedFile::READ_WRITE_AUTO_SYNC));
 
+  ICING_ASSIGN_OR_RETURN(
+      scorable_property_cache_,
+      MemoryMappedFileBackedProtoLog<ScorablePropertySetProto>::Create(
+          *filesystem_, MakeScorablePropertyCacheFilename(base_dir_)));
+
   ICING_ASSIGN_OR_RETURN(filter_cache_,
                          FileBackedVector<DocumentFilterData>::Create(
                              *filesystem_, MakeFilterCacheFilename(base_dir_),
@@ -585,6 +592,7 @@
   ICING_RETURN_IF_ERROR(ResetDocumentKeyMapper());
   ICING_RETURN_IF_ERROR(ResetDocumentIdMapper());
   ICING_RETURN_IF_ERROR(ResetDocumentAssociatedScoreCache());
+  ICING_RETURN_IF_ERROR(ResetScorablePropertyCache());
   ICING_RETURN_IF_ERROR(ResetFilterCache());
   ICING_RETURN_IF_ERROR(ResetNamespaceMapper());
   ICING_RETURN_IF_ERROR(ResetCorpusMapper());
@@ -646,10 +654,10 @@
 
     // Updates key mapper and document_id mapper with the new document
     DocumentId new_document_id = document_id_mapper_->num_elements();
+    NamespaceIdFingerprint new_doc_nsid_uri_fingerprint(
+        namespace_id, document_wrapper.document().uri());
     ICING_RETURN_IF_ERROR(document_key_mapper_->Put(
-        MakeFingerprint(namespace_id, document_wrapper.document().namespace_(),
-                        document_wrapper.document().uri()),
-        new_document_id));
+        new_doc_nsid_uri_fingerprint.EncodeToCString(), new_document_id));
     ICING_RETURN_IF_ERROR(
         document_id_mapper_->Set(new_document_id, iterator.GetOffset()));
 
@@ -673,26 +681,36 @@
     }
 
     // Update corpus maps
-    std::string corpus =
-        MakeFingerprint(namespace_id, document_wrapper.document().namespace_(),
-                        document_wrapper.document().schema());
-    ICING_ASSIGN_OR_RETURN(
-        CorpusId corpusId,
-        corpus_mapper_->GetOrPut(corpus, corpus_mapper_->num_keys()));
+    NamespaceIdFingerprint corpus_nsid_schema_fingerprint(
+        namespace_id, document_wrapper.document().schema());
+    ICING_ASSIGN_OR_RETURN(CorpusId corpus_id,
+                           corpus_mapper_->GetOrPut(
+                               corpus_nsid_schema_fingerprint.EncodeToCString(),
+                               corpus_mapper_->num_keys()));
 
     ICING_ASSIGN_OR_RETURN(CorpusAssociatedScoreData scoring_data,
-                           GetCorpusAssociatedScoreDataToUpdate(corpusId));
+                           GetCorpusAssociatedScoreDataToUpdate(corpus_id));
     scoring_data.AddDocument(
         document_wrapper.document().internal_fields().length_in_tokens());
 
     ICING_RETURN_IF_ERROR(
-        UpdateCorpusAssociatedScoreCache(corpusId, scoring_data));
+        UpdateCorpusAssociatedScoreCache(corpus_id, scoring_data));
+
+    int32_t scorable_property_cache_index = kInvalidScorablePropertyCacheIndex;
+    // Swallow the error when schema_type_id is not found, and skip updating the
+    // scorable property cache.
+    if (schema_type_id != -1) {
+      ICING_ASSIGN_OR_RETURN(scorable_property_cache_index,
+                             UpdateScorablePropertyCache(
+                                 document_wrapper.document(), schema_type_id));
+    }
 
     ICING_RETURN_IF_ERROR(UpdateDocumentAssociatedScoreCache(
         new_document_id,
         DocumentAssociatedScoreData(
-            corpusId, document_wrapper.document().score(),
+            corpus_id, document_wrapper.document().score(),
             document_wrapper.document().creation_timestamp_ms(),
+            scorable_property_cache_index,
             document_wrapper.document().internal_fields().length_in_tokens())));
 
     int64_t expiration_timestamp_ms = CalculateExpirationTimestampMs(
@@ -700,8 +718,10 @@
         document_wrapper.document().ttl_ms());
 
     ICING_RETURN_IF_ERROR(UpdateFilterCache(
-        new_document_id, DocumentFilterData(namespace_id, schema_type_id,
-                                            expiration_timestamp_ms)));
+        new_document_id,
+        DocumentFilterData(namespace_id,
+                           new_doc_nsid_uri_fingerprint.fingerprint(),
+                           schema_type_id, expiration_timestamp_ms)));
     iterator_status = iterator.Advance();
   }
 
@@ -797,6 +817,18 @@
   return libtextclassifier3::Status::OK;
 }
 
+libtextclassifier3::Status DocumentStore::ResetScorablePropertyCache() {
+  scorable_property_cache_.reset();
+  ICING_RETURN_IF_ERROR(
+      MemoryMappedFileBackedProtoLog<ScorablePropertySetProto>::Delete(
+          *filesystem_, MakeScorablePropertyCacheFilename(base_dir_)));
+  ICING_ASSIGN_OR_RETURN(
+      scorable_property_cache_,
+      MemoryMappedFileBackedProtoLog<ScorablePropertySetProto>::Create(
+          *filesystem_, MakeScorablePropertyCacheFilename(base_dir_)));
+  return libtextclassifier3::Status::OK;
+}
+
 libtextclassifier3::Status DocumentStore::ResetCorpusAssociatedScoreCache() {
   // TODO(b/139734457): Replace ptr.reset()->Delete->Create flow with Reset().
   corpus_score_cache_.reset();
@@ -908,6 +940,14 @@
   }
   Crc32 score_cache_checksum = std::move(checksum_or).ValueOrDie();
 
+  checksum_or = scorable_property_cache_->GetChecksum();
+  if (!checksum_or.ok()) {
+    ICING_LOG(ERROR) << checksum_or.status().error_message()
+                     << "Failed to compute checksum of scorable property cache";
+    return checksum_or.status();
+  }
+  Crc32 scorable_property_cache_checksum = std::move(checksum_or).ValueOrDie();
+
   // TODO(b/144458732): Implement a more robust version of TC_ASSIGN_OR_RETURN
   // that can support error logging.
   checksum_or = filter_cache_->GetChecksum();
@@ -957,6 +997,7 @@
   total_checksum.Append(std::to_string(document_key_mapper_checksum.Get()));
   total_checksum.Append(std::to_string(document_id_mapper_checksum.Get()));
   total_checksum.Append(std::to_string(score_cache_checksum.Get()));
+  total_checksum.Append(std::to_string(scorable_property_cache_checksum.Get()));
   total_checksum.Append(std::to_string(filter_cache_checksum.Get()));
   total_checksum.Append(std::to_string(namespace_mapper_checksum.Get()));
   total_checksum.Append(std::to_string(corpus_mapper_checksum.Get()));
@@ -1007,6 +1048,14 @@
   }
   Crc32 score_cache_checksum = std::move(checksum_or).ValueOrDie();
 
+  checksum_or = scorable_property_cache_->UpdateChecksum();
+  if (!checksum_or.ok()) {
+    ICING_LOG(ERROR) << checksum_or.status().error_message()
+                     << "Failed to compute checksum of scorable property cache";
+    return checksum_or.status();
+  }
+  Crc32 scorable_property_cache_checksum = std::move(checksum_or).ValueOrDie();
+
   // TODO(b/144458732): Implement a more robust version of TC_ASSIGN_OR_RETURN
   // that can support error logging.
   checksum_or = filter_cache_->UpdateChecksum();
@@ -1056,6 +1105,7 @@
   total_checksum.Append(std::to_string(document_key_mapper_checksum.Get()));
   total_checksum.Append(std::to_string(document_id_mapper_checksum.Get()));
   total_checksum.Append(std::to_string(score_cache_checksum.Get()));
+  total_checksum.Append(std::to_string(scorable_property_cache_checksum.Get()));
   total_checksum.Append(std::to_string(filter_cache_checksum.Get()));
   total_checksum.Append(std::to_string(namespace_mapper_checksum.Get()));
   total_checksum.Append(std::to_string(corpus_mapper_checksum.Get()));
@@ -1063,8 +1113,7 @@
 
   // Write the header
   DocumentStore::Header header;
-  header.magic =
-      DocumentStore::Header::GetCurrentMagic(namespace_id_fingerprint_);
+  header.magic = DocumentStore::Header::kMagic;
   header.checksum = total_checksum.Get();
 
   // This should overwrite the header.
@@ -1121,8 +1170,8 @@
   // Update ground truth first
   // TODO(b/144458732): Implement a more robust version of TC_ASSIGN_OR_RETURN
   // that can support error logging.
-  auto offset_or =
-      document_log_->WriteProto(CreateDocumentWrapper(std::move(document)));
+  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()
                      << "Failed to write document";
@@ -1152,41 +1201,49 @@
       NamespaceId namespace_id,
       namespace_mapper_->GetOrPut(name_space, namespace_mapper_->num_keys()));
 
+  NamespaceIdFingerprint new_doc_nsid_uri_fingerprint(namespace_id, uri);
+
   // Updates key mapper and document_id mapper
   ICING_RETURN_IF_ERROR(document_key_mapper_->Put(
-      MakeFingerprint(namespace_id, name_space, uri), new_document_id));
+      new_doc_nsid_uri_fingerprint.EncodeToCString(), new_document_id));
   ICING_RETURN_IF_ERROR(document_id_mapper_->Set(new_document_id, file_offset));
 
   // Update corpus maps
-  ICING_ASSIGN_OR_RETURN(CorpusId corpusId,
-                         corpus_mapper_->GetOrPut(
-                             MakeFingerprint(namespace_id, name_space, schema),
-                             corpus_mapper_->num_keys()));
+  NamespaceIdFingerprint corpus_nsid_schema_fingerprint(namespace_id, schema);
+  ICING_ASSIGN_OR_RETURN(
+      CorpusId corpus_id,
+      corpus_mapper_->GetOrPut(corpus_nsid_schema_fingerprint.EncodeToCString(),
+                               corpus_mapper_->num_keys()));
 
   ICING_ASSIGN_OR_RETURN(CorpusAssociatedScoreData scoring_data,
-                         GetCorpusAssociatedScoreDataToUpdate(corpusId));
+                         GetCorpusAssociatedScoreDataToUpdate(corpus_id));
   scoring_data.AddDocument(length_in_tokens);
 
   ICING_RETURN_IF_ERROR(
-      UpdateCorpusAssociatedScoreCache(corpusId, scoring_data));
-
-  ICING_RETURN_IF_ERROR(UpdateDocumentAssociatedScoreCache(
-      new_document_id,
-      DocumentAssociatedScoreData(corpusId, document_score,
-                                  creation_timestamp_ms, length_in_tokens)));
+      UpdateCorpusAssociatedScoreCache(corpus_id, scoring_data));
 
   ICING_ASSIGN_OR_RETURN(SchemaTypeId schema_type_id,
                          schema_store_->GetSchemaTypeId(schema));
+  ICING_ASSIGN_OR_RETURN(
+      int scorable_property_cache_index,
+      UpdateScorablePropertyCache(document_wrapper.document(), schema_type_id));
+
+  ICING_RETURN_IF_ERROR(UpdateDocumentAssociatedScoreCache(
+      new_document_id, DocumentAssociatedScoreData(
+                           corpus_id, document_score, creation_timestamp_ms,
+                           scorable_property_cache_index, length_in_tokens)));
 
   ICING_RETURN_IF_ERROR(UpdateFilterCache(
-      new_document_id, DocumentFilterData(namespace_id, schema_type_id,
-                                          expiration_timestamp_ms)));
+      new_document_id,
+      DocumentFilterData(namespace_id,
+                         new_doc_nsid_uri_fingerprint.fingerprint(),
+                         schema_type_id, expiration_timestamp_ms)));
 
   if (old_document_id_or.ok()) {
-    put_result.was_replacement = true;
     // The old document exists, copy over the usage scores and delete the old
     // document.
     DocumentId old_document_id = old_document_id_or.ValueOrDie();
+    put_result.old_document_id = old_document_id;
 
     ICING_RETURN_IF_ERROR(
         usage_store_->CloneUsageScores(/*from_document_id=*/old_document_id,
@@ -1288,14 +1345,58 @@
   return std::move(*document_wrapper.mutable_document());
 }
 
+std::unique_ptr<ScorablePropertySet> DocumentStore::GetScorablePropertySet(
+    DocumentId document_id, int64_t current_time_ms) const {
+  if (!feature_flags_.enable_scorable_properties()) {
+    return nullptr;
+  }
+
+  // Get scorable property cache index from the score_cache_
+  libtextclassifier3::StatusOr<const DocumentAssociatedScoreData*>
+      score_data_or = score_cache_->Get(document_id);
+  if (!score_data_or.ok()) {
+    return nullptr;
+  }
+  if (score_data_or.ValueOrDie()->scorable_property_cache_index() ==
+      kInvalidScorablePropertyCacheIndex) {
+    return nullptr;
+  }
+
+  // Get ScorablePropertySetProto.
+  libtextclassifier3::StatusOr<ScorablePropertySetProto>
+      scorable_property_set_proto_or = scorable_property_cache_->Read(
+          score_data_or.ValueOrDie()->scorable_property_cache_index());
+  if (!scorable_property_set_proto_or.ok()) {
+    return nullptr;
+  }
+
+  // Get schema type id.
+  auto document_filter_data_optional =
+      GetAliveDocumentFilterData(document_id, current_time_ms);
+  if (!document_filter_data_optional) {
+    return nullptr;
+  }
+
+  libtextclassifier3::StatusOr<std::unique_ptr<ScorablePropertySet>>
+      scorable_property_set_or = ScorablePropertySet::Create(
+          std::move(scorable_property_set_proto_or.ValueOrDie()),
+          document_filter_data_optional.value().schema_type_id(),
+          schema_store_);
+  if (!scorable_property_set_or.ok()) {
+    return nullptr;
+  }
+  return std::move(scorable_property_set_or.ValueOrDie());
+}
+
 libtextclassifier3::StatusOr<DocumentId> DocumentStore::GetDocumentId(
     const std::string_view name_space, const std::string_view uri) const {
   auto namespace_id_or = namespace_mapper_->Get(name_space);
   libtextclassifier3::Status status = namespace_id_or.status();
   if (status.ok()) {
     NamespaceId namespace_id = namespace_id_or.ValueOrDie();
-    auto document_id_or = document_key_mapper_->Get(
-        MakeFingerprint(namespace_id, name_space, uri));
+    NamespaceIdFingerprint doc_nsid_uri_fingerprint(namespace_id, uri);
+    auto document_id_or =
+        document_key_mapper_->Get(doc_nsid_uri_fingerprint.EncodeToCString());
     status = document_id_or.status();
     if (status.ok()) {
       // Guaranteed to have a DocumentId now
@@ -1308,16 +1409,9 @@
 }
 
 libtextclassifier3::StatusOr<DocumentId> DocumentStore::GetDocumentId(
-    const NamespaceFingerprintIdentifier& namespace_fingerprint_identifier)
-    const {
-  if (!namespace_id_fingerprint_) {
-    return absl_ports::FailedPreconditionError(
-        "Cannot lookup document id by namespace id + fingerprint without "
-        "enabling it on uri_mapper");
-  }
-
+    const NamespaceIdFingerprint& doc_namespace_id_uri_fingerprint) const {
   auto document_id_or = document_key_mapper_->Get(
-      namespace_fingerprint_identifier.EncodeToCString());
+      doc_namespace_id_uri_fingerprint.EncodeToCString());
   if (document_id_or.ok()) {
     return document_id_or.ValueOrDie();
   }
@@ -1365,6 +1459,27 @@
   return GetNonExpiredDocumentFilterData(document_id, current_time_ms);
 }
 
+std::optional<DocumentFilterData>
+DocumentStore::GetNonDeletedDocumentFilterData(DocumentId document_id) const {
+  if (IsDeleted(document_id)) {
+    return std::nullopt;
+  }
+
+  auto filter_data_or = filter_cache_->GetCopy(document_id);
+  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.
+    return std::nullopt;
+  }
+
+  // At this point, it's guaranteed that the document has not been deleted. It
+  // could still be expired, but the filter data is guaranteed to be valid here.
+  return std::move(filter_data_or).ValueOrDie();
+}
+
 bool DocumentStore::IsDeleted(DocumentId document_id) const {
   auto file_offset_or = document_id_mapper_->Get(document_id);
   if (!file_offset_or.ok()) {
@@ -1389,7 +1504,7 @@
     // 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 true since the
+    // DoesDocumentExist(WithStatus). Regardless, return std::nullopt since the
     // document doesn't exist.
     return std::nullopt;
   }
@@ -1451,7 +1566,8 @@
     const std::string_view name_space, const std::string_view schema) const {
   ICING_ASSIGN_OR_RETURN(NamespaceId namespace_id,
                          namespace_mapper_->Get(name_space));
-  return corpus_mapper_->Get(MakeFingerprint(namespace_id, name_space, schema));
+  NamespaceIdFingerprint corpus_nsid_schema_fp(namespace_id, schema);
+  return corpus_mapper_->Get(corpus_nsid_schema_fp.EncodeToCString());
 }
 
 libtextclassifier3::StatusOr<int32_t> DocumentStore::GetResultGroupingEntryId(
@@ -1699,6 +1815,7 @@
   ICING_RETURN_IF_ERROR(document_key_mapper_->PersistToDisk());
   ICING_RETURN_IF_ERROR(document_id_mapper_->PersistToDisk());
   ICING_RETURN_IF_ERROR(score_cache_->PersistToDisk());
+  ICING_RETURN_IF_ERROR(scorable_property_cache_->PersistToDisk());
   ICING_RETURN_IF_ERROR(filter_cache_->PersistToDisk());
   ICING_RETURN_IF_ERROR(namespace_mapper_->PersistToDisk());
   ICING_RETURN_IF_ERROR(usage_store_->PersistToDisk());
@@ -1725,6 +1842,8 @@
       GetValueOrDefault(document_id_mapper_->GetDiskUsage(), -1));
   storage_info.set_score_cache_size(
       GetValueOrDefault(score_cache_->GetDiskUsage(), -1));
+  storage_info.set_scorable_property_cache_size(
+      GetValueOrDefault(scorable_property_cache_->GetDiskUsage(), -1));
   storage_info.set_filter_cache_size(
       GetValueOrDefault(filter_cache_->GetDiskUsage(), -1));
   storage_info.set_namespace_id_mapper_size(
@@ -1965,6 +2084,44 @@
   return libtextclassifier3::Status::OK;
 }
 
+libtextclassifier3::Status DocumentStore::RegenerateScorablePropertyCache(
+    const std::unordered_set<SchemaTypeId>& schema_type_ids) {
+  if (schema_type_ids.empty()) {
+    return libtextclassifier3::Status::OK;
+  }
+
+  int64_t current_time_ms = clock_.GetSystemTimeMilliseconds();
+  for (DocumentId document_id = 0;
+       document_id < document_id_mapper_->num_elements(); ++document_id) {
+    if (!GetAliveDocumentFilterData(document_id, current_time_ms)) {
+      continue;
+    }
+    // Guaranteed that the document exists now.
+    ICING_ASSIGN_OR_RETURN(const DocumentFilterData* filter_data,
+                           filter_cache_->Get(document_id));
+    SchemaTypeId schema_type_id = filter_data->schema_type_id();
+    if (schema_type_ids.find(schema_type_id) == schema_type_ids.end()) {
+      continue;
+    }
+
+    ICING_ASSIGN_OR_RETURN(DocumentProto document, Get(document_id));
+    int32_t scorable_property_cache_index = kInvalidScorablePropertyCacheIndex;
+    ICING_ASSIGN_OR_RETURN(
+        scorable_property_cache_index,
+        UpdateScorablePropertyCache(document, schema_type_id));
+
+    // Update the score_cache_ with the new scorable property cache index.
+    ICING_ASSIGN_OR_RETURN(
+        typename FileBackedVector<DocumentAssociatedScoreData>::MutableView
+            doc_score_data_view,
+        score_cache_->GetMutable(document_id));
+    doc_score_data_view.Get().set_scorable_property_cache_index(
+        scorable_property_cache_index);
+  }
+
+  return libtextclassifier3::Status::OK;
+}
+
 // TODO(b/121227117): Implement Optimize()
 libtextclassifier3::Status DocumentStore::Optimize() {
   return libtextclassifier3::Status::OK;
@@ -1983,11 +2140,11 @@
 
   ICING_ASSIGN_OR_RETURN(
       auto doc_store_create_result,
-      DocumentStore::Create(filesystem_, new_directory, &clock_, schema_store_,
-                            /*force_recovery_and_revalidate_documents=*/false,
-                            namespace_id_fingerprint_, pre_mapping_fbv_,
-                            use_persistent_hash_map_, compression_level_,
-                            /*initialize_stats=*/nullptr));
+      DocumentStore::Create(
+          filesystem_, new_directory, &clock_, schema_store_, &feature_flags_,
+          /*force_recovery_and_revalidate_documents=*/false, pre_mapping_fbv_,
+          use_persistent_hash_map_, compression_level_,
+          /*initialize_stats=*/nullptr));
   std::unique_ptr<DocumentStore> new_doc_store =
       std::move(doc_store_create_result.document_store);
 
@@ -1998,18 +2155,23 @@
   UsageStore::UsageScores default_usage;
   OptimizeResult result;
   result.document_id_old_to_new.resize(document_cnt, kInvalidDocumentId);
-  result.dead_blob_handles = std::move(potentially_optimizable_blob_handles);
 
-  // Get the blob property map from the schema store.
-  auto type_blob_property_map_or = schema_store_->ConstructBlobPropertyMap();
-  if (num_documents() == 0) {
-    // If we fail to retrieve this map when there *are* documents in
-    // doc store, then something is seriously wrong. Return error.
-    return result;
-  }
+  result.dead_blob_handles = std::move(potentially_optimizable_blob_handles);
   std::unordered_map<std::string, std::vector<std::string>>
-      type_blob_property_map =
-          std::move(type_blob_property_map_or).ValueOrDie();
+      type_blob_property_map;
+  if (!result.dead_blob_handles.empty()) {
+    // Get the blob property map from the schema store.
+    if (num_documents() == 0) {
+      return result;
+    }
+    auto type_blob_property_map_or = schema_store_->ConstructBlobPropertyMap();
+    if (!type_blob_property_map_or.ok()) {
+      // If we fail to retrieve this map when there *are* documents in
+      // doc store, then something is seriously wrong. Return error.
+      return type_blob_property_map_or.status();
+    }
+    type_blob_property_map = std::move(type_blob_property_map_or).ValueOrDie();
+  }
 
   int64_t current_time_ms = clock_.GetSystemTimeMilliseconds();
   for (DocumentId document_id = 0; document_id < document_cnt; document_id++) {
@@ -2164,6 +2326,8 @@
                          document_id_mapper_->GetElementsFileSize());
   ICING_ASSIGN_OR_RETURN(const int64_t score_cache_file_size,
                          score_cache_->GetElementsFileSize());
+  ICING_ASSIGN_OR_RETURN(const int64_t scorable_property_cache_file_size,
+                         scorable_property_cache_->GetElementsFileSize());
   ICING_ASSIGN_OR_RETURN(const int64_t filter_cache_file_size,
                          filter_cache_->GetElementsFileSize());
   ICING_ASSIGN_OR_RETURN(const int64_t corpus_score_cache_file_size,
@@ -2186,6 +2350,7 @@
 
   int64_t total_size = document_log_file_size + document_key_mapper_size +
                        document_id_mapper_file_size + score_cache_file_size +
+                       scorable_property_cache_file_size +
                        filter_cache_file_size + corpus_score_cache_file_size +
                        usage_store_file_size;
 
@@ -2219,15 +2384,20 @@
 
   // Resets the score cache entry
   ICING_RETURN_IF_ERROR(UpdateDocumentAssociatedScoreCache(
-      document_id, DocumentAssociatedScoreData(kInvalidCorpusId,
-                                               /*document_score=*/-1,
-                                               /*creation_timestamp_ms=*/-1,
-                                               /*length_in_tokens=*/0)));
+      document_id,
+      DocumentAssociatedScoreData(
+          kInvalidCorpusId,
+          /*document_score=*/-1,
+          /*creation_timestamp_ms=*/-1,
+          /*scorable_property_cache_index=*/kInvalidScorablePropertyCacheIndex,
+          /*length_in_tokens=*/0)));
 
   // Resets the filter cache entry
   ICING_RETURN_IF_ERROR(UpdateFilterCache(
-      document_id, DocumentFilterData(kInvalidNamespaceId, kInvalidSchemaTypeId,
-                                      /*expiration_timestamp_ms=*/-1)));
+      document_id,
+      DocumentFilterData(kInvalidNamespaceId, /*uri_fingerprint=*/0,
+                         kInvalidSchemaTypeId,
+                         /*expiration_timestamp_ms=*/-1)));
 
   // Clears the usage scores.
   return usage_store_->DeleteUsageScores(document_id);
@@ -2296,5 +2466,28 @@
   return debug_info;
 }
 
+libtextclassifier3::StatusOr<int> DocumentStore::UpdateScorablePropertyCache(
+    const DocumentProto& document, SchemaTypeId schema_type_id) {
+  if (!feature_flags_.enable_scorable_properties()) {
+    return kInvalidScorablePropertyCacheIndex;
+  }
+  ICING_ASSIGN_OR_RETURN(
+      const std::vector<ScorablePropertyManager::ScorablePropertyInfo>*
+          ordered_scorable_property_info,
+      schema_store_->GetOrderedScorablePropertyInfo(schema_type_id));
+  if (ordered_scorable_property_info == nullptr ||
+      ordered_scorable_property_info->empty()) {
+    // No scorable property defined under the schema config of the
+    // schema_type_id.
+    return kInvalidScorablePropertyCacheIndex;
+  }
+  ICING_ASSIGN_OR_RETURN(
+      std::unique_ptr<ScorablePropertySet> scorable_property_set,
+      ScorablePropertySet::Create(document, schema_type_id, schema_store_));
+
+  return scorable_property_cache_->Write(
+      scorable_property_set->GetScorablePropertySetProto());
+}
+
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/store/document-store.h b/icing/store/document-store.h
index 0caead0..7246235 100644
--- a/icing/store/document-store.h
+++ b/icing/store/document-store.h
@@ -17,6 +17,7 @@
 
 #include <cstdint>
 #include <memory>
+#include <optional>
 #include <string>
 #include <string_view>
 #include <unordered_set>
@@ -24,12 +25,15 @@
 
 #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/file-backed-vector.h"
 #include "icing/file/filesystem.h"
+#include "icing/file/memory-mapped-file-backed-proto-log.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/proto/debug.pb.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/document_wrapper.pb.h"
+#include "icing/proto/internal/scorable_property_set.pb.h"
 #include "icing/proto/logging.pb.h"
 #include "icing/proto/optimize.pb.h"
 #include "icing/proto/persist.pb.h"
@@ -43,7 +47,7 @@
 #include "icing/store/document-filter-data.h"
 #include "icing/store/document-id.h"
 #include "icing/store/key-mapper.h"
-#include "icing/store/namespace-fingerprint-identifier.h"
+#include "icing/store/namespace-id-fingerprint.h"
 #include "icing/store/namespace-id.h"
 #include "icing/store/usage-store.h"
 #include "icing/tokenization/language-segmenter.h"
@@ -52,6 +56,7 @@
 #include "icing/util/data-loss.h"
 #include "icing/util/document-validator.h"
 #include "icing/util/fingerprint-util.h"
+#include "icing/util/scorable_property_set.h"
 
 namespace icing {
 namespace lib {
@@ -60,19 +65,15 @@
 class DocumentStore {
  public:
   struct Header {
-    static int32_t GetCurrentMagic(bool namespace_id_fingerprint) {
-      return namespace_id_fingerprint ? kNewMagic : kOldMagic;
-    }
+    // Previously used magic numbers, please avoid reusing those:
+    // [0x1b99c8b0, 0x3e005b5e]
+    static constexpr int32_t kMagic = 0x8a32cd1f;
 
     // Holds the magic as a quick sanity check against file corruption.
     int32_t magic;
 
     // Checksum of the DocumentStore's sub-component's checksums.
     uint32_t checksum;
-
-   private:
-    static constexpr int32_t kOldMagic = 0x746f7265;
-    static constexpr int32_t kNewMagic = 0x1b99c8b0;
   };
 
   struct OptimizeInfo {
@@ -147,8 +148,8 @@
   static libtextclassifier3::StatusOr<DocumentStore::CreateResult> Create(
       const Filesystem* filesystem, const std::string& base_dir,
       const Clock* clock, const SchemaStore* schema_store,
-      bool force_recovery_and_revalidate_documents,
-      bool namespace_id_fingerprint, bool pre_mapping_fbv,
+      const FeatureFlags* feature_flags,
+      bool force_recovery_and_revalidate_documents, bool pre_mapping_fbv,
       bool use_persistent_hash_map, int32_t compression_level,
       InitializeStatsProto* initialize_stats);
 
@@ -182,16 +183,20 @@
   //
   //  Returns:
   //   - On success, a PutResult with the DocumentId of the newly added document
-  //     and a bool indicating whether this is a new document or a replacement
-  //     of an existing document.
+  //     and the old DocumentId before replacement. If this is a new document,
+  //     then old DocumentId will be kInvalidDocumentId.
   //   - RESOURCE_EXHAUSTED if exceeds maximum number of allowed documents
   //   - FAILED_PRECONDITION if schema hasn't been set yet
   //   - NOT_FOUND if the schema_type or a property config of the document
   //     doesn't exist in schema
   //   - INTERNAL_ERROR on IO error
   struct PutResult {
+    DocumentId old_document_id = kInvalidDocumentId;
     DocumentId new_document_id = kInvalidDocumentId;
-    bool was_replacement = false;
+
+    bool was_replacement() const {
+      return old_document_id != kInvalidDocumentId;
+    }
   };
   libtextclassifier3::StatusOr<PutResult> Put(
       const DocumentProto& document, int32_t num_tokens = 0,
@@ -225,6 +230,19 @@
   libtextclassifier3::StatusOr<DocumentProto> Get(
       DocumentId document_id, bool clear_internal_fields = true) const;
 
+  // Returns the ScorablePropertySet of the document specified by the
+  // DocumentId.
+  //
+  // Returns:
+  //   - ScorablePropertySet on success
+  //   - nullptr when the ScorablePropertySet fails to be created, it could be
+  //     due to that:
+  //     - |document_id| is invalid, or
+  //     - no ScorablePropertySetProto is found for the document in the cache
+  //     - internal IO error
+  std::unique_ptr<ScorablePropertySet> GetScorablePropertySet(
+      DocumentId document_id, int64_t current_time_ms) const;
+
   // Returns all namespaces which have at least 1 active document (not deleted
   // or expired). Order of namespaces is undefined.
   std::vector<std::string> GetAllNamespaces() const;
@@ -283,7 +301,7 @@
       std::string_view name_space, std::string_view uri) const;
 
   // Helper method to find a DocumentId that is associated with the given
-  // NamespaceFingerprintIdentifier.
+  // NamespaceIdFingerprint.
   //
   // NOTE: The DocumentId may refer to a invalid document (deleted
   // or expired). Callers can call DoesDocumentExist(document_id) to ensure it
@@ -294,8 +312,7 @@
   //   NOT_FOUND if the key doesn't exist
   //   INTERNAL_ERROR on IO error
   libtextclassifier3::StatusOr<DocumentId> GetDocumentId(
-      const NamespaceFingerprintIdentifier& namespace_fingerprint_identifier)
-      const;
+      const NamespaceIdFingerprint& doc_namespace_id_uri_fingerprint) const;
 
   // Returns the CorpusId associated with the given namespace and schema.
   //
@@ -359,8 +376,8 @@
   libtextclassifier3::StatusOr<CorpusAssociatedScoreData>
   GetCorpusAssociatedScoreData(CorpusId corpus_id) const;
 
-  // Gets the document filter data if a document exists. Otherwise, will get a
-  // false optional.
+  // Gets the document filter data if a document exists and is not expired.
+  // Otherwise, will get a false optional.
   //
   // Existence means it hasn't been deleted and it hasn't expired yet.
   //
@@ -370,6 +387,32 @@
   std::optional<DocumentFilterData> GetAliveDocumentFilterData(
       DocumentId document_id, int64_t current_time_ms) const;
 
+  // Gets the document filter data if a document has not been deleted. If the
+  // document is expired but not deleted, will still return a valid document
+  // filter data. Otherwise, will get a false optional.
+  //
+  // Returns:
+  //   True:DocumentFilterData  if the given document exists.
+  //   False                    if the given document has been deleted.
+  std::optional<DocumentFilterData> GetNonDeletedDocumentFilterData(
+      DocumentId document_id) const;
+
+  // Gets the SchemaTypeId of a document.
+  //
+  // Returns:
+  //   SchemaTypeId on success
+  //   kInvalidSchemaTypeId if the document is deleted or expired.
+  SchemaTypeId GetSchemaTypeId(DocumentId document_id,
+                               int64_t current_time_ms) const {
+    std::optional<DocumentFilterData> document_filter_data_optional =
+        GetAliveDocumentFilterData(document_id, current_time_ms);
+    if (document_filter_data_optional) {
+      return document_filter_data_optional.value().schema_type_id();
+    } else {
+      return kInvalidSchemaTypeId;
+    }
+  }
+
   // Gets the usage scores of a document.
   //
   // Returns:
@@ -454,6 +497,15 @@
       const SchemaStore* schema_store,
       const SchemaStore::SetSchemaResult& set_schema_result);
 
+  // Re-generates the scorable property cache for documents with the given
+  // schema types.
+  //
+  // Returns:
+  //   OK on success
+  //   INTERNAL_ERROR on IO error
+  libtextclassifier3::Status RegenerateScorablePropertyCache(
+      const std::unordered_set<SchemaTypeId>& schema_type_ids);
+
   // Reduces internal file sizes by reclaiming space of deleted documents and
   // regenerating derived files.
   //
@@ -544,13 +596,14 @@
   explicit DocumentStore(const Filesystem* filesystem,
                          std::string_view base_dir, const Clock* clock,
                          const SchemaStore* schema_store,
-                         bool namespace_id_fingerprint, bool pre_mapping_fbv,
-                         bool use_persistent_hash_map,
+                         const FeatureFlags* feature_flags,
+                         bool pre_mapping_fbv, bool use_persistent_hash_map,
                          int32_t compression_level);
 
   const Filesystem* const filesystem_;
   const std::string base_dir_;
   const Clock& clock_;
+  const FeatureFlags& feature_flags_;  // Does not own.
 
   // Handles the ground truth schema and all of the derived data off of the
   // schema
@@ -559,10 +612,6 @@
   // Used to validate incoming documents
   DocumentValidator document_validator_;
 
-  // Whether to use namespace id or namespace name to build up fingerprint for
-  // document_key_mapper_ and corpus_mapper_.
-  bool namespace_id_fingerprint_;
-
   // Flag indicating whether memory map max possible file size for underlying
   // FileBackedVector before growing the actual file size.
   bool pre_mapping_fbv_;
@@ -592,8 +641,14 @@
   //   - Document score
   //   - Document creation timestamp in seconds
   //   - Document length in number of tokens
+  //   - Index of the ScorablePropertySetProto at the scorable_property_cache_
   std::unique_ptr<FileBackedVector<DocumentAssociatedScoreData>> score_cache_;
 
+  // A cache of document scorable properties. The ground truth of the data is
+  // DocumentProto stored in document_log_.
+  std::unique_ptr<MemoryMappedFileBackedProtoLog<ScorablePropertySetProto>>
+      scorable_property_cache_;
+
   // A cache of data, indexed by DocumentId, used to filter documents. Currently
   // contains:
   //   - NamespaceId
@@ -688,6 +743,12 @@
   // Returns OK or any IO errors.
   libtextclassifier3::Status ResetDocumentAssociatedScoreCache();
 
+  // Resets the unique_ptr to the |scorable_property_cache_|, deletes the
+  // underlying file, and re-creates a new instance of it.
+  //
+  // Returns OK or any IO errors.
+  libtextclassifier3::Status ResetScorablePropertyCache();
+
   // Resets the unique_ptr to the corpus_score_cache, deletes the underlying
   // file, and re-creates a new instance of the corpus_score_cache.
   //
@@ -823,12 +884,19 @@
       google::protobuf::RepeatedPtrField<DocumentDebugInfoProto::CorpusInfo>>
   CollectCorpusInfo() const;
 
-  // Build fingerprint for the keys of document_key_mapper_ and corpus_mapper_.
-  // Note that namespace_id_fingerprint_ controls the way that a fingerprint is
-  // built.
-  std::string MakeFingerprint(NamespaceId namespace_id,
-                              std::string_view namespace_,
-                              std::string_view uri_or_schema) const;
+  // Extracts the ScorablePropertySetProto from the |document| and add it to
+  // the |scorable_property_cache_|.
+  //
+  // Returns:
+  //     - Index of the newly inserted ScorablePropertySetProto in the
+  //       |scorable_property_cache_|.
+  //     - kInvalidScorablePropertyCacheIndex if the schema contains no
+  //       scorable properties.
+  //     - INVALID_ARGUMENT if |schema_type_id| is invalid, or the converted
+  //       ScorablePropertySetProto exceeds the size limit of 16MiB.
+  //     - INTERNAL_ERROR on IO error.
+  libtextclassifier3::StatusOr<int> UpdateScorablePropertyCache(
+      const DocumentProto& document, SchemaTypeId schema_type_id);
 };
 
 }  // namespace lib
diff --git a/icing/store/document-store_benchmark.cc b/icing/store/document-store_benchmark.cc
index 3523e14..08f3e13 100644
--- a/icing/store/document-store_benchmark.cc
+++ b/icing/store/document-store_benchmark.cc
@@ -30,7 +30,9 @@
 #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/proto/document.pb.h"
 #include "icing/proto/persist.pb.h"
 #include "icing/proto/schema.pb.h"
@@ -39,6 +41,7 @@
 #include "icing/schema/schema-store.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"
 
@@ -108,13 +111,14 @@
       .Build();
 }
 
-std::unique_ptr<SchemaStore> CreateSchemaStore(Filesystem filesystem,
-                                               const std::string directory,
-                                               const Clock* clock) {
+std::unique_ptr<SchemaStore> CreateSchemaStore(
+    Filesystem filesystem, const std::string directory, const Clock* clock,
+    const FeatureFlags& feature_flags) {
   const std::string schema_store_dir = directory + "/schema";
   filesystem.CreateDirectoryRecursively(schema_store_dir.data());
   std::unique_ptr<SchemaStore> schema_store =
-      SchemaStore::Create(&filesystem, schema_store_dir, clock).ValueOrDie();
+      SchemaStore::Create(&filesystem, schema_store_dir, clock, &feature_flags)
+          .ValueOrDie();
 
   auto set_schema_status = schema_store->SetSchema(
       CreateSchema(), /*ignore_errors_and_delete_documents=*/false,
@@ -128,17 +132,18 @@
 
 libtextclassifier3::StatusOr<DocumentStore::CreateResult> CreateDocumentStore(
     const Filesystem* filesystem, const std::string& base_dir,
-    const Clock* clock, const SchemaStore* schema_store) {
+    const Clock* clock, const SchemaStore* schema_store,
+    const FeatureFlags& feature_flags) {
   return DocumentStore::Create(
-      filesystem, base_dir, clock, schema_store,
+      filesystem, base_dir, clock, schema_store, &feature_flags,
       /*force_recovery_and_revalidate_documents=*/false,
-      /*namespace_id_fingerprint=*/true, /*pre_mapping_fbv=*/false,
-      /*use_persistent_hash_map=*/true,
-      PortableFileBackedProtoLog<DocumentWrapper>::kDeflateCompressionLevel,
+      /*pre_mapping_fbv=*/false, /*use_persistent_hash_map=*/true,
+      PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
       /*initialize_stats=*/nullptr);
 }
 
 void BM_DoesDocumentExistBenchmark(benchmark::State& state) {
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   Filesystem filesystem;
   Clock clock;
 
@@ -147,13 +152,13 @@
 
   std::string document_store_dir = directory + "/store";
   std::unique_ptr<SchemaStore> schema_store =
-      CreateSchemaStore(filesystem, directory, &clock);
+      CreateSchemaStore(filesystem, directory, &clock, feature_flags);
 
   filesystem.CreateDirectoryRecursively(document_store_dir.data());
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem, document_store_dir, &clock,
-                          schema_store.get()));
+                          schema_store.get(), feature_flags));
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
@@ -181,6 +186,7 @@
 BENCHMARK(BM_DoesDocumentExistBenchmark);
 
 void BM_Put(benchmark::State& state) {
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   Filesystem filesystem;
   Clock clock;
 
@@ -189,13 +195,13 @@
 
   std::string document_store_dir = directory + "/store";
   std::unique_ptr<SchemaStore> schema_store =
-      CreateSchemaStore(filesystem, directory, &clock);
+      CreateSchemaStore(filesystem, directory, &clock, feature_flags);
 
   filesystem.CreateDirectoryRecursively(document_store_dir.data());
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem, document_store_dir, &clock,
-                          schema_store.get()));
+                          schema_store.get(), feature_flags));
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
@@ -210,6 +216,7 @@
 BENCHMARK(BM_Put);
 
 void BM_GetSameDocument(benchmark::State& state) {
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   Filesystem filesystem;
   Clock clock;
 
@@ -218,13 +225,13 @@
 
   std::string document_store_dir = directory + "/store";
   std::unique_ptr<SchemaStore> schema_store =
-      CreateSchemaStore(filesystem, directory, &clock);
+      CreateSchemaStore(filesystem, directory, &clock, feature_flags);
 
   filesystem.CreateDirectoryRecursively(document_store_dir.data());
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem, document_store_dir, &clock,
-                          schema_store.get()));
+                          schema_store.get(), feature_flags));
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
@@ -237,6 +244,7 @@
 BENCHMARK(BM_GetSameDocument);
 
 void BM_Delete(benchmark::State& state) {
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   Filesystem filesystem;
   Clock clock;
 
@@ -245,13 +253,13 @@
 
   std::string document_store_dir = directory + "/store";
   std::unique_ptr<SchemaStore> schema_store =
-      CreateSchemaStore(filesystem, directory, &clock);
+      CreateSchemaStore(filesystem, directory, &clock, feature_flags);
 
   filesystem.CreateDirectoryRecursively(document_store_dir.data());
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem, document_store_dir, &clock,
-                          schema_store.get()));
+                          schema_store.get(), feature_flags));
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
@@ -269,6 +277,7 @@
 BENCHMARK(BM_Delete);
 
 void BM_Create(benchmark::State& state) {
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   Filesystem filesystem;
   Clock clock;
 
@@ -276,7 +285,7 @@
   std::string document_store_dir = directory + "/store";
 
   std::unique_ptr<SchemaStore> schema_store =
-      CreateSchemaStore(filesystem, directory, &clock);
+      CreateSchemaStore(filesystem, directory, &clock, feature_flags);
 
   // Create an initial document store and put some data in.
   {
@@ -286,7 +295,7 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
         CreateDocumentStore(&filesystem, document_store_dir, &clock,
-                            schema_store.get()));
+                            schema_store.get(), feature_flags));
     std::unique_ptr<DocumentStore> document_store =
         std::move(create_result.document_store);
 
@@ -301,13 +310,15 @@
   filesystem.CreateDirectoryRecursively(document_store_dir.data());
 
   for (auto s : state) {
-    benchmark::DoNotOptimize(CreateDocumentStore(
-        &filesystem, document_store_dir, &clock, schema_store.get()));
+    benchmark::DoNotOptimize(
+        CreateDocumentStore(&filesystem, document_store_dir, &clock,
+                            schema_store.get(), feature_flags));
   }
 }
 BENCHMARK(BM_Create);
 
 void BM_UpdateChecksum(benchmark::State& state) {
+  FeatureFlags feature_flags = GetTestFeatureFlags();
   Filesystem filesystem;
   Clock clock;
 
@@ -316,13 +327,13 @@
 
   std::string document_store_dir = directory + "/store";
   std::unique_ptr<SchemaStore> schema_store =
-      CreateSchemaStore(filesystem, directory, &clock);
+      CreateSchemaStore(filesystem, directory, &clock, feature_flags);
 
   filesystem.CreateDirectoryRecursively(document_store_dir.data());
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
       CreateDocumentStore(&filesystem, document_store_dir, &clock,
-                          schema_store.get()));
+                          schema_store.get(), feature_flags));
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
 
diff --git a/icing/store/document-store_test.cc b/icing/store/document-store_test.cc
index 776f19f..775a4e0 100644
--- a/icing/store/document-store_test.cc
+++ b/icing/store/document-store_test.cc
@@ -23,10 +23,12 @@
 #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"
 #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"
@@ -36,6 +38,7 @@
 #include "icing/proto/debug.pb.h"
 #include "icing/proto/document.pb.h"
 #include "icing/proto/document_wrapper.pb.h"
+#include "icing/proto/internal/scorable_property_set.pb.h"
 #include "icing/proto/logging.pb.h"
 #include "icing/proto/schema.pb.h"
 #include "icing/proto/storage.pb.h"
@@ -45,19 +48,22 @@
 #include "icing/schema/schema-store.h"
 #include "icing/store/corpus-associated-scoring-data.h"
 #include "icing/store/corpus-id.h"
+#include "icing/store/document-associated-score-data.h"
 #include "icing/store/document-filter-data.h"
 #include "icing/store/document-id.h"
 #include "icing/store/document-log-creator.h"
-#include "icing/store/namespace-fingerprint-identifier.h"
+#include "icing/store/namespace-id-fingerprint.h"
 #include "icing/store/namespace-id.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.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/crc32.h"
+#include "icing/util/icu-data-file-helper.h"
+#include "icing/util/scorable_property_set.h"
 #include "unicode/uloc.h"
 
 namespace icing {
@@ -75,7 +81,9 @@
 using ::testing::IsEmpty;
 using ::testing::IsFalse;
 using ::testing::IsTrue;
+using ::testing::Ne;
 using ::testing::Not;
+using ::testing::Pointee;
 using ::testing::Return;
 using ::testing::UnorderedElementsAre;
 
@@ -123,16 +131,36 @@
                    sizeof(PortableFileBackedProtoLog<DocumentWrapper>::Header));
 }
 
+ScorablePropertyProto BuildScorablePropertyProtoFromBoolean(
+    bool boolean_value) {
+  ScorablePropertyProto scorable_property;
+  scorable_property.add_boolean_values(boolean_value);
+  return scorable_property;
+}
+
+ScorablePropertyProto BuildScorablePropertyProtoFromInt64(
+    const std::vector<int64_t>& int64_values) {
+  ScorablePropertyProto scorable_property;
+  scorable_property.mutable_int64_values()->Add(int64_values.begin(),
+                                                int64_values.end());
+  return scorable_property;
+}
+
+ScorablePropertyProto BuildScorablePropertyProtoFromDouble(
+    const std::vector<double>& double_values) {
+  ScorablePropertyProto scorable_property;
+  scorable_property.mutable_double_values()->Add(double_values.begin(),
+                                                 double_values.end());
+  return scorable_property;
+}
+
 struct DocumentStoreTestParam {
-  bool namespace_id_fingerprint;
   bool pre_mapping_fbv;
   bool use_persistent_hash_map;
 
-  explicit DocumentStoreTestParam(bool namespace_id_fingerprint_in,
-                                  bool pre_mapping_fbv_in,
+  explicit DocumentStoreTestParam(bool pre_mapping_fbv_in,
                                   bool use_persistent_hash_map_in)
-      : namespace_id_fingerprint(namespace_id_fingerprint_in),
-        pre_mapping_fbv(pre_mapping_fbv_in),
+      : pre_mapping_fbv(pre_mapping_fbv_in),
         use_persistent_hash_map(use_persistent_hash_map_in) {}
 };
 
@@ -149,6 +177,7 @@
             .SetSchema("email")
             .AddStringProperty("subject", "subject foo")
             .AddStringProperty("body", "body bar")
+            .AddDoubleProperty("score", 1.5, 2.5)
             .SetScore(document1_score_)
             .SetCreationTimestampMs(
                 document1_creation_timestamp_)  // A random timestamp
@@ -160,6 +189,7 @@
             .SetSchema("email")
             .AddStringProperty("subject", "subject foo 2")
             .AddStringProperty("body", "body bar 2")
+            .AddDoubleProperty("score", 3.5, 4.5)
             .SetScore(document2_score_)
             .SetCreationTimestampMs(
                 document2_creation_timestamp_)  // A random timestamp
@@ -168,6 +198,7 @@
   }
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     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.
@@ -178,7 +209,7 @@
       std::string icu_data_file_path =
           GetTestFilePath("icing/icu.dat");
       ICING_ASSERT_OK(
-          icu_data_file_helper::SetUpICUDataFile(icu_data_file_path));
+          icu_data_file_helper::SetUpIcuDataFile(icu_data_file_path));
     }
 
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
@@ -200,11 +231,17 @@
                                      .SetName("body")
                                      .SetDataTypeString(TERM_MATCH_EXACT,
                                                         TOKENIZER_PLAIN)
-                                     .SetCardinality(CARDINALITY_OPTIONAL)))
+                                     .SetCardinality(CARDINALITY_OPTIONAL))
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("score")
+                            .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                            .SetScorableType(SCORABLE_TYPE_ENABLED)
+                            .SetCardinality(CARDINALITY_REPEATED)))
             .Build();
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     ASSERT_THAT(schema_store_->SetSchema(
                     schema, /*ignore_errors_and_delete_documents=*/false,
                     /*allow_circular_schema_definitions=*/false),
@@ -229,8 +266,7 @@
     const std::string header_file =
         absl_ports::StrCat(document_store_dir_, "/document_store_header");
     DocumentStore::Header header;
-    header.magic = DocumentStore::Header::GetCurrentMagic(
-        GetParam().namespace_id_fingerprint);
+    header.magic = DocumentStore::Header::kMagic;
     header.checksum = 10;  // Arbitrary garbage checksum
     filesystem_.DeleteFile(header_file.c_str());
     filesystem_.Write(header_file.c_str(), &header, sizeof(header));
@@ -240,14 +276,14 @@
       const Filesystem* filesystem, const std::string& base_dir,
       const Clock* clock, const SchemaStore* schema_store) {
     return DocumentStore::Create(
-        filesystem, base_dir, clock, schema_store,
+        filesystem, base_dir, clock, schema_store, feature_flags_.get(),
         /*force_recovery_and_revalidate_documents=*/false,
-        GetParam().namespace_id_fingerprint, GetParam().pre_mapping_fbv,
-        GetParam().use_persistent_hash_map,
-        PortableFileBackedProtoLog<DocumentWrapper>::kDeflateCompressionLevel,
+        GetParam().pre_mapping_fbv, GetParam().use_persistent_hash_map,
+        PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
         /*initialize_stats=*/nullptr);
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   const Filesystem filesystem_;
   const std::string test_dir_;
   FakeClock fake_clock_;
@@ -306,11 +342,13 @@
   // Both documents have namespace of "icing"
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
                              doc_store->Put(test_document1_));
-  EXPECT_FALSE(put_result1.was_replacement);
+  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(test_document2_));
-  EXPECT_FALSE(put_result2.was_replacement);
+  EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result2.was_replacement());
   DocumentId document_id2 = put_result2.new_document_id;
 
   EXPECT_THAT(doc_store->Get(document_id1),
@@ -341,11 +379,13 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
                              doc_store->Put(foo_document));
-  EXPECT_FALSE(put_result1.was_replacement);
+  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(DocumentProto(bar_document)));
-  EXPECT_FALSE(put_result2.was_replacement);
+  EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result2.was_replacement());
   DocumentId document_id2 = put_result2.new_document_id;
 
   EXPECT_THAT(doc_store->Get(document_id1),
@@ -370,12 +410,14 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
                              doc_store->Put(document1));
-  EXPECT_FALSE(put_result1.was_replacement);
+  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(document2));
   DocumentId document_id2 = put_result2.new_document_id;
-  EXPECT_TRUE(put_result2.was_replacement);
+  EXPECT_THAT(put_result2.old_document_id, Eq(document_id1));
+  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),
@@ -388,8 +430,9 @@
   document3.set_uri("another/uri/1");
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result3,
                              doc_store->Put(document3));
-  EXPECT_FALSE(put_result3.was_replacement);
-  EXPECT_THAT(put_result3.new_document_id, Not(document_id1));
+  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());
 }
 
 TEST_P(DocumentStoreTest, IsDocumentExistingWithoutStatus) {
@@ -402,11 +445,13 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
                              doc_store->Put(test_document1_));
-  EXPECT_FALSE(put_result1.was_replacement);
+  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(test_document2_));
-  EXPECT_FALSE(put_result2.was_replacement);
+  EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result2.was_replacement());
   DocumentId document_id2 = put_result2.new_document_id;
 
   EXPECT_TRUE(doc_store->GetAliveDocumentFilterData(
@@ -498,7 +543,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
                              doc_store->Put(test_document1_));
-  EXPECT_FALSE(put_result.was_replacement);
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result.was_replacement());
   DocumentId document_id = put_result.new_document_id;
 
   DocumentId invalid_document_id_negative = -1;
@@ -767,7 +813,8 @@
   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_));
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
 
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
@@ -787,7 +834,8 @@
                                        .Build();
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult email_put_result1,
                              document_store->Put(email_document_1));
-  EXPECT_FALSE(email_put_result1.was_replacement);
+  EXPECT_THAT(email_put_result1.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(email_put_result1.was_replacement());
   DocumentId email_1_document_id = email_put_result1.new_document_id;
 
   DocumentProto email_document_2 = DocumentBuilder()
@@ -797,7 +845,8 @@
                                        .Build();
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult email_put_result2,
                              document_store->Put(email_document_2));
-  EXPECT_FALSE(email_put_result2.was_replacement);
+  EXPECT_THAT(email_put_result2.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(email_put_result2.was_replacement());
   DocumentId email_2_document_id = email_put_result2.new_document_id;
 
   DocumentProto message_document = DocumentBuilder()
@@ -807,7 +856,8 @@
                                        .Build();
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult message_put_result,
                              document_store->Put(message_document));
-  EXPECT_FALSE(message_put_result.was_replacement);
+  EXPECT_THAT(message_put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(message_put_result.was_replacement());
   DocumentId message_document_id = message_put_result.new_document_id;
 
   DocumentProto person_document = DocumentBuilder()
@@ -817,7 +867,8 @@
                                       .Build();
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult person_put_result,
                              document_store->Put(person_document));
-  EXPECT_FALSE(person_put_result.was_replacement);
+  EXPECT_THAT(person_put_result.old_document_id, Eq(kInvalidDocumentId));
+  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
@@ -905,7 +956,8 @@
   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_));
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
 
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
@@ -936,11 +988,13 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult email_put_result,
                                document_store->Put(email_document));
-    EXPECT_FALSE(email_put_result.was_replacement);
+    EXPECT_THAT(email_put_result.old_document_id, Eq(kInvalidDocumentId));
+    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));
-    EXPECT_FALSE(message_put_result.was_replacement);
+    EXPECT_THAT(message_put_result.old_document_id, Eq(kInvalidDocumentId));
+    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 =
@@ -1002,7 +1056,8 @@
   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_));
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
 
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
@@ -1033,11 +1088,13 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult email_put_result,
                                document_store->Put(email_document));
-    EXPECT_FALSE(email_put_result.was_replacement);
+    EXPECT_THAT(email_put_result.old_document_id, Eq(kInvalidDocumentId));
+    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));
-    EXPECT_FALSE(message_put_result.was_replacement);
+    EXPECT_THAT(message_put_result.old_document_id, Eq(kInvalidDocumentId));
+    EXPECT_FALSE(message_put_result.was_replacement());
     message_document_id = message_put_result.new_document_id;
 
     // Delete "email". "message" documents should still be retrievable.
@@ -1475,12 +1532,14 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::PutResult put_result1,
         doc_store->Put(DocumentProto(test_document1_), /*num_tokens=*/4));
-    EXPECT_FALSE(put_result1.was_replacement);
+    EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
+    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));
-    EXPECT_FALSE(put_result2.was_replacement);
+    EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
+    EXPECT_FALSE(put_result2.was_replacement());
     document_id2 = put_result2.new_document_id;
     EXPECT_THAT(doc_store->Get(document_id1),
                 IsOkAndHolds(EqualsProto(test_document1_)));
@@ -1489,18 +1548,34 @@
     // Checks derived score cache
     EXPECT_THAT(
         doc_store->GetDocumentAssociatedScoreData(document_id1),
-        IsOkAndHolds(DocumentAssociatedScoreData(
+        IsOkAndHolds(EqualsDocumentAssociatedScoreData(
             /*corpus_id=*/0, document1_score_, document1_creation_timestamp_,
-            /*length_in_tokens=*/4)));
+            /*length_in_tokens=*/4,
+            /*has_valid_scorable_property_cache_index=*/true)));
     EXPECT_THAT(
         doc_store->GetDocumentAssociatedScoreData(document_id2),
-        IsOkAndHolds(DocumentAssociatedScoreData(
+        IsOkAndHolds(EqualsDocumentAssociatedScoreData(
             /*corpus_id=*/0, document2_score_, document2_creation_timestamp_,
-            /*length_in_tokens=*/4)));
+            /*length_in_tokens=*/4,
+            /*has_valid_scorable_property_cache_index=*/true)));
     EXPECT_THAT(doc_store->GetCorpusAssociatedScoreData(/*corpus_id=*/0),
                 IsOkAndHolds(CorpusAssociatedScoreData(
                     /*num_docs=*/2, /*sum_length_in_tokens=*/8)));
 
+    // Checks derived scorable property set cache
+    std::unique_ptr<ScorablePropertySet> scorable_property_set_doc1 =
+        doc_store->GetScorablePropertySet(
+            document_id1, fake_clock_.GetSystemTimeMilliseconds());
+    EXPECT_THAT(
+        scorable_property_set_doc1->GetScorablePropertyProto("score"),
+        Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({1.5, 2.5}))));
+    std::unique_ptr<ScorablePropertySet> scorable_property_set_doc2 =
+        doc_store->GetScorablePropertySet(
+            document_id2, fake_clock_.GetSystemTimeMilliseconds());
+    EXPECT_THAT(
+        scorable_property_set_doc2->GetScorablePropertyProto("score"),
+        Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({3.5, 4.5}))));
+
     // Delete document 1
     EXPECT_THAT(doc_store->Delete("icing", "email/1",
                                   fake_clock_.GetSystemTimeMilliseconds()),
@@ -1540,20 +1615,33 @@
       DocumentFilterData doc_filter_data,
       doc_store->GetAliveDocumentFilterData(
           document_id2, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(doc_filter_data,
-              Eq(DocumentFilterData(
-                  /*namespace_id=*/0,
-                  /*schema_type_id=*/0, document2_expiration_timestamp_)));
+  EXPECT_THAT(
+      doc_filter_data,
+      Eq(DocumentFilterData(
+          /*namespace_id=*/0, tc3farmhash::Fingerprint64(test_document2_.uri()),
+          /*schema_type_id=*/0, document2_expiration_timestamp_)));
 
   // Checks derived score cache
   EXPECT_THAT(
       doc_store->GetDocumentAssociatedScoreData(document_id2),
-      IsOkAndHolds(DocumentAssociatedScoreData(
+      IsOkAndHolds(EqualsDocumentAssociatedScoreData(
           /*corpus_id=*/0, document2_score_, document2_creation_timestamp_,
-          /*length_in_tokens=*/4)));
+          /*length_in_tokens=*/4,
+          /*has_valid_scorable_property_cache_index=*/true)));
   EXPECT_THAT(doc_store->GetCorpusAssociatedScoreData(/*corpus_id=*/0),
               IsOkAndHolds(CorpusAssociatedScoreData(
                   /*num_docs=*/1, /*sum_length_in_tokens=*/4)));
+
+  // Checks derived scorable property set cache
+  EXPECT_EQ(doc_store->GetScorablePropertySet(
+                document_id1, fake_clock_.GetSystemTimeMilliseconds()),
+            nullptr);
+  std::unique_ptr<ScorablePropertySet> scorable_property_set_doc2 =
+      doc_store->GetScorablePropertySet(
+          document_id2, fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(
+      scorable_property_set_doc2->GetScorablePropertyProto("score"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({3.5, 4.5}))));
 }
 
 TEST_P(DocumentStoreTest, ShouldRecoverFromCorruptDerivedFile) {
@@ -1570,12 +1658,14 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::PutResult put_result1,
         doc_store->Put(DocumentProto(test_document1_), /*num_tokens=*/4));
-    EXPECT_FALSE(put_result1.was_replacement);
+    EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
+    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));
-    EXPECT_FALSE(put_result2.was_replacement);
+    EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
+    EXPECT_FALSE(put_result2.was_replacement());
     document_id2 = put_result2.new_document_id;
     EXPECT_THAT(doc_store->Get(document_id1),
                 IsOkAndHolds(EqualsProto(test_document1_)));
@@ -1584,17 +1674,33 @@
     // Checks derived score cache
     EXPECT_THAT(
         doc_store->GetDocumentAssociatedScoreData(document_id1),
-        IsOkAndHolds(DocumentAssociatedScoreData(
+        IsOkAndHolds(EqualsDocumentAssociatedScoreData(
             /*corpus_id=*/0, document1_score_, document1_creation_timestamp_,
-            /*length_in_tokens=*/4)));
+            /*length_in_tokens=*/4,
+            /*has_valid_scorable_property_cache_index=*/true)));
     EXPECT_THAT(
         doc_store->GetDocumentAssociatedScoreData(document_id2),
-        IsOkAndHolds(DocumentAssociatedScoreData(
+        IsOkAndHolds(EqualsDocumentAssociatedScoreData(
             /*corpus_id=*/0, document2_score_, document2_creation_timestamp_,
-            /*length_in_tokens=*/4)));
+            /*length_in_tokens=*/4,
+            /*has_valid_scorable_property_cache_index=*/true)));
     EXPECT_THAT(doc_store->GetCorpusAssociatedScoreData(/*corpus_id=*/0),
                 IsOkAndHolds(CorpusAssociatedScoreData(
                     /*num_docs=*/2, /*sum_length_in_tokens=*/8)));
+    // Checks derived scorable property set cache
+    std::unique_ptr<ScorablePropertySet> scorable_property_set_doc1 =
+        doc_store->GetScorablePropertySet(
+            document_id1, fake_clock_.GetSystemTimeMilliseconds());
+    EXPECT_THAT(
+        scorable_property_set_doc1->GetScorablePropertyProto("score"),
+        Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({1.5, 2.5}))));
+    std::unique_ptr<ScorablePropertySet> scorable_property_set_doc2 =
+        doc_store->GetScorablePropertySet(
+            document_id2, fake_clock_.GetSystemTimeMilliseconds());
+    EXPECT_THAT(
+        scorable_property_set_doc2->GetScorablePropertyProto("score"),
+        Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({3.5, 4.5}))));
+
     // Delete document 1
     EXPECT_THAT(doc_store->Delete("icing", "email/1",
                                   fake_clock_.GetSystemTimeMilliseconds()),
@@ -1645,17 +1751,19 @@
       DocumentFilterData doc_filter_data,
       doc_store->GetAliveDocumentFilterData(
           document_id2, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(doc_filter_data,
-              Eq(DocumentFilterData(
-                  /*namespace_id=*/0,
-                  /*schema_type_id=*/0, document2_expiration_timestamp_)));
+  EXPECT_THAT(
+      doc_filter_data,
+      Eq(DocumentFilterData(
+          /*namespace_id=*/0, tc3farmhash::Fingerprint64(test_document2_.uri()),
+          /*schema_type_id=*/0, document2_expiration_timestamp_)));
 
   // Checks derived score cache
   EXPECT_THAT(
       doc_store->GetDocumentAssociatedScoreData(document_id2),
-      IsOkAndHolds(DocumentAssociatedScoreData(
+      IsOkAndHolds(EqualsDocumentAssociatedScoreData(
           /*corpus_id=*/0, document2_score_, document2_creation_timestamp_,
-          /*length_in_tokens=*/4)));
+          /*length_in_tokens=*/4,
+          /*has_valid_scorable_property_cache_index=*/true)));
   EXPECT_THAT(doc_store->GetCorpusAssociatedScoreData(/*corpus_id=*/0),
               IsOkAndHolds(CorpusAssociatedScoreData(
                   /*num_docs=*/1, /*sum_length_in_tokens=*/4)));
@@ -1669,6 +1777,17 @@
       doc_store->GetUsageScores(document_id2,
                                 fake_clock_.GetSystemTimeMilliseconds()));
   EXPECT_THAT(actual_scores, Eq(expected_scores));
+
+  // Checks derived scorable property set cache
+  EXPECT_EQ(doc_store->GetScorablePropertySet(
+                document_id1, fake_clock_.GetSystemTimeMilliseconds()),
+            nullptr);
+  std::unique_ptr<ScorablePropertySet> scorable_property_set_doc2 =
+      doc_store->GetScorablePropertySet(
+          document_id2, fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(
+      scorable_property_set_doc2->GetScorablePropertyProto("score"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({3.5, 4.5}))));
 }
 
 TEST_P(DocumentStoreTest, ShouldRecoverFromDiscardDerivedFiles) {
@@ -1685,12 +1804,14 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::PutResult put_result1,
         doc_store->Put(DocumentProto(test_document1_), /*num_tokens=*/4));
-    EXPECT_FALSE(put_result1.was_replacement);
+    EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
+    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));
-    EXPECT_FALSE(put_result2.was_replacement);
+    EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
+    EXPECT_FALSE(put_result2.was_replacement());
     document_id2 = put_result2.new_document_id;
     EXPECT_THAT(doc_store->Get(document_id1),
                 IsOkAndHolds(EqualsProto(test_document1_)));
@@ -1699,17 +1820,34 @@
     // Checks derived score cache
     EXPECT_THAT(
         doc_store->GetDocumentAssociatedScoreData(document_id1),
-        IsOkAndHolds(DocumentAssociatedScoreData(
+        IsOkAndHolds(EqualsDocumentAssociatedScoreData(
             /*corpus_id=*/0, document1_score_, document1_creation_timestamp_,
-            /*length_in_tokens=*/4)));
+            /*length_in_tokens=*/4,
+            /*has_valid_scorable_property_cache_index=*/true)));
     EXPECT_THAT(
         doc_store->GetDocumentAssociatedScoreData(document_id2),
-        IsOkAndHolds(DocumentAssociatedScoreData(
+        IsOkAndHolds(EqualsDocumentAssociatedScoreData(
             /*corpus_id=*/0, document2_score_, document2_creation_timestamp_,
-            /*length_in_tokens=*/4)));
+            /*length_in_tokens=*/4,
+            /*has_valid_scorable_property_cache_index=*/true)));
     EXPECT_THAT(doc_store->GetCorpusAssociatedScoreData(/*corpus_id=*/0),
                 IsOkAndHolds(CorpusAssociatedScoreData(
                     /*num_docs=*/2, /*sum_length_in_tokens=*/8)));
+
+    // Checks derived scorable property set cache
+    std::unique_ptr<ScorablePropertySet> scorable_property_set_doc1 =
+        doc_store->GetScorablePropertySet(
+            document_id1, fake_clock_.GetSystemTimeMilliseconds());
+    EXPECT_THAT(
+        scorable_property_set_doc1->GetScorablePropertyProto("score"),
+        Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({1.5, 2.5}))));
+    std::unique_ptr<ScorablePropertySet> scorable_property_set_doc2 =
+        doc_store->GetScorablePropertySet(
+            document_id2, fake_clock_.GetSystemTimeMilliseconds());
+    EXPECT_THAT(
+        scorable_property_set_doc2->GetScorablePropertyProto("score"),
+        Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({3.5, 4.5}))));
+
     // Delete document 1
     EXPECT_THAT(doc_store->Delete("icing", "email/1",
                                   fake_clock_.GetSystemTimeMilliseconds()),
@@ -1747,17 +1885,19 @@
       DocumentFilterData doc_filter_data,
       doc_store->GetAliveDocumentFilterData(
           document_id2, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(doc_filter_data,
-              Eq(DocumentFilterData(
-                  /*namespace_id=*/0,
-                  /*schema_type_id=*/0, document2_expiration_timestamp_)));
+  EXPECT_THAT(
+      doc_filter_data,
+      Eq(DocumentFilterData(
+          /*namespace_id=*/0, tc3farmhash::Fingerprint64(test_document2_.uri()),
+          /*schema_type_id=*/0, document2_expiration_timestamp_)));
 
   // Checks derived score cache.
   EXPECT_THAT(
       doc_store->GetDocumentAssociatedScoreData(document_id2),
-      IsOkAndHolds(DocumentAssociatedScoreData(
+      IsOkAndHolds(EqualsDocumentAssociatedScoreData(
           /*corpus_id=*/0, document2_score_, document2_creation_timestamp_,
-          /*length_in_tokens=*/4)));
+          /*length_in_tokens=*/4,
+          /*has_valid_scorable_property_cache_index=*/true)));
   EXPECT_THAT(doc_store->GetCorpusAssociatedScoreData(/*corpus_id=*/0),
               IsOkAndHolds(CorpusAssociatedScoreData(
                   /*num_docs=*/1, /*sum_length_in_tokens=*/4)));
@@ -1771,6 +1911,17 @@
       doc_store->GetUsageScores(document_id2,
                                 fake_clock_.GetSystemTimeMilliseconds()));
   EXPECT_THAT(actual_scores, Eq(expected_scores));
+
+  // Checks derived scorable property set cache
+  EXPECT_EQ(doc_store->GetScorablePropertySet(
+                document_id1, fake_clock_.GetSystemTimeMilliseconds()),
+            nullptr);
+  std::unique_ptr<ScorablePropertySet> scorable_property_set_doc2 =
+      doc_store->GetScorablePropertySet(
+          document_id2, fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(
+      scorable_property_set_doc2->GetScorablePropertyProto("score"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({3.5, 4.5}))));
 }
 
 TEST_P(DocumentStoreTest, ShouldRecoverFromBadChecksum) {
@@ -1787,12 +1938,14 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::PutResult put_result1,
         doc_store->Put(DocumentProto(test_document1_), /*num_tokens=*/4));
-    EXPECT_FALSE(put_result1.was_replacement);
+    EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
+    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));
-    EXPECT_FALSE(put_result2.was_replacement);
+    EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
+    EXPECT_FALSE(put_result2.was_replacement());
     document_id2 = put_result2.new_document_id;
     EXPECT_THAT(doc_store->Get(document_id1),
                 IsOkAndHolds(EqualsProto(test_document1_)));
@@ -1801,17 +1954,34 @@
     // Checks derived score cache
     EXPECT_THAT(
         doc_store->GetDocumentAssociatedScoreData(document_id1),
-        IsOkAndHolds(DocumentAssociatedScoreData(
+        IsOkAndHolds(EqualsDocumentAssociatedScoreData(
             /*corpus_id=*/0, document1_score_, document1_creation_timestamp_,
-            /*length_in_tokens=*/4)));
+            /*length_in_tokens=*/4,
+            /*has_valid_scorable_property_cache_index=*/true)));
     EXPECT_THAT(
         doc_store->GetDocumentAssociatedScoreData(document_id2),
-        IsOkAndHolds(DocumentAssociatedScoreData(
+        IsOkAndHolds(EqualsDocumentAssociatedScoreData(
             /*corpus_id=*/0, document2_score_, document2_creation_timestamp_,
-            /*length_in_tokens=*/4)));
+            /*length_in_tokens=*/4,
+            /*has_valid_scorable_property_cache_index=*/true)));
     EXPECT_THAT(doc_store->GetCorpusAssociatedScoreData(/*corpus_id=*/0),
                 IsOkAndHolds(CorpusAssociatedScoreData(
                     /*num_docs=*/2, /*sum_length_in_tokens=*/8)));
+
+    // Checks derived scorable property set cache
+    std::unique_ptr<ScorablePropertySet> scorable_property_set_doc1 =
+        doc_store->GetScorablePropertySet(
+            document_id1, fake_clock_.GetSystemTimeMilliseconds());
+    EXPECT_THAT(
+        scorable_property_set_doc1->GetScorablePropertyProto("score"),
+        Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({1.5, 2.5}))));
+    std::unique_ptr<ScorablePropertySet> scorable_property_set_doc2 =
+        doc_store->GetScorablePropertySet(
+            document_id2, fake_clock_.GetSystemTimeMilliseconds());
+    EXPECT_THAT(
+        scorable_property_set_doc2->GetScorablePropertyProto("score"),
+        Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({3.5, 4.5}))));
+
     EXPECT_THAT(doc_store->Delete("icing", "email/1",
                                   fake_clock_.GetSystemTimeMilliseconds()),
                 IsOk());
@@ -1840,19 +2010,32 @@
       DocumentFilterData doc_filter_data,
       doc_store->GetAliveDocumentFilterData(
           document_id2, fake_clock_.GetSystemTimeMilliseconds()));
-  EXPECT_THAT(doc_filter_data,
-              Eq(DocumentFilterData(
-                  /*namespace_id=*/0,
-                  /*schema_type_id=*/0, document2_expiration_timestamp_)));
+  EXPECT_THAT(
+      doc_filter_data,
+      Eq(DocumentFilterData(
+          /*namespace_id=*/0, tc3farmhash::Fingerprint64(test_document2_.uri()),
+          /*schema_type_id=*/0, document2_expiration_timestamp_)));
   // Checks derived score cache
   EXPECT_THAT(
       doc_store->GetDocumentAssociatedScoreData(document_id2),
-      IsOkAndHolds(DocumentAssociatedScoreData(
+      IsOkAndHolds(EqualsDocumentAssociatedScoreData(
           /*corpus_id=*/0, document2_score_, document2_creation_timestamp_,
-          /*length_in_tokens=*/4)));
+          /*length_in_tokens=*/4,
+          /*has_valid_scorable_property_cache_index=*/true)));
   EXPECT_THAT(doc_store->GetCorpusAssociatedScoreData(/*corpus_id=*/0),
               IsOkAndHolds(CorpusAssociatedScoreData(
                   /*num_docs=*/1, /*sum_length_in_tokens=*/4)));
+
+  // Checks derived scorable property set cache
+  EXPECT_EQ(doc_store->GetScorablePropertySet(
+                document_id1, fake_clock_.GetSystemTimeMilliseconds()),
+            nullptr);
+  std::unique_ptr<ScorablePropertySet> scorable_property_set_doc2 =
+      doc_store->GetScorablePropertySet(
+          document_id2, fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(
+      scorable_property_set_doc2->GetScorablePropertyProto("score"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({3.5, 4.5}))));
 }
 
 TEST_P(DocumentStoreTest, GetStorageInfo) {
@@ -1912,7 +2095,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
                              doc_store->Put(DocumentProto(test_document1_)));
-  EXPECT_FALSE(put_result1.was_replacement);
+  EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
+  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));
 
@@ -1923,7 +2107,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
                              doc_store->Put(DocumentProto(test_document2_)));
-  EXPECT_FALSE(put_result2.was_replacement);
+  EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
+  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));
 }
@@ -2176,24 +2361,28 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
       doc_store->Put(DocumentProto(document1), /*num_tokens=*/5));
-  EXPECT_FALSE(put_result1.was_replacement);
+  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(DocumentProto(document2), /*num_tokens=*/7));
-  EXPECT_FALSE(put_result2.was_replacement);
+  EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result2.was_replacement());
   DocumentId document_id2 = put_result2.new_document_id;
 
   EXPECT_THAT(
       doc_store->GetDocumentAssociatedScoreData(document_id1),
-      IsOkAndHolds(DocumentAssociatedScoreData(
+      IsOkAndHolds(EqualsDocumentAssociatedScoreData(
           /*corpus_id=*/0, document1_score_, document1_creation_timestamp_,
-          /*length_in_tokens=*/5)));
+          /*length_in_tokens=*/5,
+          /*has_valid_scorable_property_cache_index=*/true)));
   EXPECT_THAT(
       doc_store->GetDocumentAssociatedScoreData(document_id2),
-      IsOkAndHolds(DocumentAssociatedScoreData(
+      IsOkAndHolds(EqualsDocumentAssociatedScoreData(
           /*corpus_id=*/0, document2_score_, document2_creation_timestamp_,
-          /*length_in_tokens=*/7)));
+          /*length_in_tokens=*/7,
+          /*has_valid_scorable_property_cache_index=*/true)));
 }
 
 TEST_P(DocumentStoreTest, GetDocumentAssociatedScoreDataDifferentCorpus) {
@@ -2224,24 +2413,28 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result1,
       doc_store->Put(DocumentProto(document1), /*num_tokens=*/5));
-  EXPECT_FALSE(put_result1.was_replacement);
+  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(DocumentProto(document2), /*num_tokens=*/7));
-  EXPECT_FALSE(put_result2.was_replacement);
+  EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result2.was_replacement());
   DocumentId document_id2 = put_result2.new_document_id;
 
   EXPECT_THAT(
       doc_store->GetDocumentAssociatedScoreData(document_id1),
-      IsOkAndHolds(DocumentAssociatedScoreData(
+      IsOkAndHolds(EqualsDocumentAssociatedScoreData(
           /*corpus_id=*/0, document1_score_, document1_creation_timestamp_,
-          /*length_in_tokens=*/5)));
+          /*length_in_tokens=*/5,
+          /*has_valid_scorable_property_cache_index=*/true)));
   EXPECT_THAT(
       doc_store->GetDocumentAssociatedScoreData(document_id2),
-      IsOkAndHolds(DocumentAssociatedScoreData(
+      IsOkAndHolds(EqualsDocumentAssociatedScoreData(
           /*corpus_id=*/1, document2_score_, document2_creation_timestamp_,
-          /*length_in_tokens=*/7)));
+          /*length_in_tokens=*/7,
+          /*has_valid_scorable_property_cache_index=*/true)));
 }
 
 TEST_P(DocumentStoreTest, NonexistentDocumentAssociatedScoreDataNotFound) {
@@ -2278,17 +2471,19 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
                              doc_store->Put(DocumentProto(test_document1_)));
-  EXPECT_FALSE(put_result.was_replacement);
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  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,
-                  /*schema_type_id=*/0, document1_expiration_timestamp_)));
+  EXPECT_THAT(
+      doc_filter_data,
+      Eq(DocumentFilterData(
+          /*namespace_id=*/0, tc3farmhash::Fingerprint64(test_document1_.uri()),
+          /*schema_type_id=*/0, document1_expiration_timestamp_)));
 
   ICING_ASSERT_OK(doc_store->Delete("icing", "email/1",
                                     fake_clock_.GetSystemTimeMilliseconds()));
@@ -2308,25 +2503,50 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
       doc_store->Put(DocumentProto(test_document1_), /*num_tokens=*/4));
-  EXPECT_FALSE(put_result.was_replacement);
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result.was_replacement());
   DocumentId document_id = put_result.new_document_id;
 
   EXPECT_THAT(doc_store->GetDocumentAssociatedScoreData(document_id),
-              IsOkAndHolds(DocumentAssociatedScoreData(
+              IsOkAndHolds(EqualsDocumentAssociatedScoreData(
                   /*corpus_id=*/0,
                   /*document_score=*/document1_score_,
                   /*creation_timestamp_ms=*/document1_creation_timestamp_,
-                  /*length_in_tokens=*/4)));
+                  /*length_in_tokens=*/4,
+                  /*has_valid_scorable_property_cache_index=*/true)));
 
   ICING_ASSERT_OK(doc_store->Delete("icing", "email/1",
                                     fake_clock_.GetSystemTimeMilliseconds()));
   // Associated entry of the deleted document is removed.
-  EXPECT_THAT(
-      doc_store->GetDocumentAssociatedScoreData(document_id),
-      IsOkAndHolds(DocumentAssociatedScoreData(kInvalidCorpusId,
-                                               /*document_score=*/-1,
-                                               /*creation_timestamp_ms=*/-1,
-                                               /*length_in_tokens=*/0)));
+  EXPECT_THAT(doc_store->GetDocumentAssociatedScoreData(document_id),
+              IsOkAndHolds(EqualsDocumentAssociatedScoreData(
+                  kInvalidCorpusId,
+                  /*document_score=*/-1,
+                  /*creation_timestamp_ms=*/-1,
+                  /*length_in_tokens=*/0,
+                  /*has_valid_scorable_property_cache_index=*/false)));
+}
+
+TEST_P(DocumentStoreTest, DeleteClearsScorablePropertyCache) {
+  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(test_document1_, /*num_tokens=*/4));
+  DocumentId document_id = put_result1.new_document_id;
+  EXPECT_NE(doc_store->GetScorablePropertySet(
+                document_id, fake_clock_.GetSystemTimeMilliseconds()),
+            nullptr);
+  ICING_ASSERT_OK(doc_store->Delete("icing", "email/1",
+                                    fake_clock_.GetSystemTimeMilliseconds()));
+  // Scorable property set is not found for deleted documents.
+  EXPECT_EQ(doc_store->GetScorablePropertySet(
+                document_id, fake_clock_.GetSystemTimeMilliseconds()),
+            nullptr);
 }
 
 TEST_P(DocumentStoreTest, DeleteShouldPreventUsageScores) {
@@ -2339,7 +2559,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
                              doc_store->Put(DocumentProto(test_document1_)));
-  EXPECT_FALSE(put_result.was_replacement);
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result.was_replacement());
   DocumentId document_id = put_result.new_document_id;
 
   // Report usage with type 1.
@@ -2390,7 +2611,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
                              doc_store->Put(document));
-  EXPECT_FALSE(put_result.was_replacement);
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  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)
@@ -2441,16 +2663,18 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
                              doc_store->Put(document));
-  EXPECT_FALSE(put_result.was_replacement);
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  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,
-                                   /*schema_type_id=*/0,
-                                   /*expiration_timestamp_ms=*/1100)));
+  EXPECT_THAT(
+      doc_filter_data,
+      Eq(DocumentFilterData(
+          /*namespace_id=*/0, tc3farmhash::Fingerprint64(document.uri()),
+          /*schema_type_id=*/0, /*expiration_timestamp_ms=*/1100)));
 }
 
 TEST_P(DocumentStoreTest, ExpirationTimestampIsInt64MaxIfTtlIsZero) {
@@ -2470,7 +2694,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
                              doc_store->Put(document));
-  EXPECT_FALSE(put_result.was_replacement);
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result.was_replacement());
   DocumentId document_id = put_result.new_document_id;
 
   ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
@@ -2481,7 +2706,7 @@
   EXPECT_THAT(
       doc_filter_data,
       Eq(DocumentFilterData(
-          /*namespace_id=*/0,
+          /*namespace_id=*/0, tc3farmhash::Fingerprint64(document.uri()),
           /*schema_type_id=*/0,
           /*expiration_timestamp_ms=*/std::numeric_limits<int64_t>::max())));
 }
@@ -2504,7 +2729,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
                              doc_store->Put(document));
-  EXPECT_FALSE(put_result.was_replacement);
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result.was_replacement());
   DocumentId document_id = put_result.new_document_id;
 
   ICING_ASSERT_HAS_VALUE_AND_ASSIGN(
@@ -2515,7 +2741,7 @@
   EXPECT_THAT(
       doc_filter_data,
       Eq(DocumentFilterData(
-          /*namespace_id=*/0,
+          /*namespace_id=*/0, tc3farmhash::Fingerprint64(document.uri()),
           /*schema_type_id=*/0,
           /*expiration_timestamp_ms=*/std::numeric_limits<int64_t>::max())));
 }
@@ -2542,7 +2768,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult put_result,
       doc_store->Put(document_without_creation_timestamp));
-  EXPECT_FALSE(put_result.was_replacement);
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result.was_replacement());
   DocumentId document_id = put_result.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentProto document_with_creation_timestamp,
@@ -2576,24 +2803,28 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
                              doc_store->Put(document1));
-  EXPECT_FALSE(put_result1.was_replacement);
+  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(document2));
-  EXPECT_FALSE(put_result2.was_replacement);
+  EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result2.was_replacement());
   DocumentId document_id2 = put_result2.new_document_id;
 
   EXPECT_THAT(doc_store->GetDocumentAssociatedScoreData(document_id1),
-              IsOkAndHolds(DocumentAssociatedScoreData(
+              IsOkAndHolds(EqualsDocumentAssociatedScoreData(
                   /*corpus_id=*/0,
                   /*document_score=*/0, /*creation_timestamp_ms=*/0,
-                  /*length_in_tokens=*/0)));
+                  /*length_in_tokens=*/0,
+                  /*has_valid_scorable_property_cache_index=*/true)));
 
   EXPECT_THAT(doc_store->GetDocumentAssociatedScoreData(document_id2),
-              IsOkAndHolds(DocumentAssociatedScoreData(
+              IsOkAndHolds(EqualsDocumentAssociatedScoreData(
                   /*corpus_id=*/0,
                   /*document_score=*/5, /*creation_timestamp_ms=*/0,
-                  /*length_in_tokens=*/0)));
+                  /*length_in_tokens=*/0,
+                  /*has_valid_scorable_property_cache_index=*/true)));
 }
 
 TEST_P(DocumentStoreTest, GetChecksumDoesntUpdateStoredChecksum) {
@@ -2634,7 +2865,8 @@
                                          &fake_clock_, schema_store_.get()));
   EXPECT_FALSE(create_result.derived_files_regenerated);
 
-  std::unique_ptr<DocumentStore> document_store_two = std::move(create_result.document_store);
+  std::unique_ptr<DocumentStore> document_store_two =
+      std::move(create_result.document_store);
   EXPECT_THAT(document_store_two->GetChecksum(), IsOkAndHolds(checksum));
   EXPECT_THAT(document_store_two->UpdateChecksum(), IsOkAndHolds(checksum));
   EXPECT_THAT(document_store_two->GetChecksum(), IsOkAndHolds(checksum));
@@ -2726,7 +2958,8 @@
     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_));
+        SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                            feature_flags_.get()));
     SchemaProto schema =
         SchemaBuilder()
             .AddType(SchemaTypeConfigBuilder().SetType("email"))
@@ -2752,7 +2985,8 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::PutResult email_put_result,
         document_store->Put(DocumentProto(email_document)));
-    EXPECT_FALSE(email_put_result.was_replacement);
+    EXPECT_THAT(email_put_result.old_document_id, Eq(kInvalidDocumentId));
+    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)));
@@ -2760,6 +2994,8 @@
         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();
@@ -2768,7 +3004,8 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::PutResult message_put_result,
         document_store->Put(DocumentProto(message_document)));
-    EXPECT_FALSE(message_put_result.was_replacement);
+    EXPECT_THAT(message_put_result.old_document_id, Eq(kInvalidDocumentId));
+    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)));
@@ -2776,6 +3013,8 @@
         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();
@@ -2791,7 +3030,8 @@
   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_));
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
 
   SchemaProto schema = SchemaBuilder()
                            .AddType(SchemaTypeConfigBuilder().SetType("email"))
@@ -2822,6 +3062,8 @@
   EXPECT_THAT(email_data.schema_type_id(), Eq(email_schema_type_id));
   // Make sure that all the other fields are stll 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));
 
@@ -2835,6 +3077,8 @@
   EXPECT_THAT(message_data.schema_type_id(), Eq(-1));
   // Make sure that all the other fields are stll 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));
 }
@@ -2853,7 +3097,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
   ICING_EXPECT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/false));
@@ -2886,23 +3131,29 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_put_result,
       document_store->Put(DocumentProto(email_document)));
-  EXPECT_FALSE(email_put_result.was_replacement);
+  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;
   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)));
-  EXPECT_FALSE(message_put_result.was_replacement);
+  EXPECT_THAT(message_put_result.old_document_id, Eq(kInvalidDocumentId));
+  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));
 
   // Rearrange the schema types. Since SchemaTypeId is assigned based on order,
@@ -2932,12 +3183,16 @@
       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));
 }
 
@@ -2958,7 +3213,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
   ICING_EXPECT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/false));
@@ -2990,7 +3246,9 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_without_subject_put_result,
       document_store->Put(DocumentProto(email_without_subject)));
-  EXPECT_FALSE(email_without_subject_put_result.was_replacement);
+  EXPECT_THAT(email_without_subject_put_result.old_document_id,
+              Eq(kInvalidDocumentId));
+  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),
@@ -2999,7 +3257,9 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_with_subject_put_result,
       document_store->Put(DocumentProto(email_with_subject)));
-  EXPECT_FALSE(email_with_subject_put_result.was_replacement);
+  EXPECT_THAT(email_with_subject_put_result.old_document_id,
+              Eq(kInvalidDocumentId));
+  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),
@@ -3040,7 +3300,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
   ICING_EXPECT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/false));
@@ -3070,14 +3331,16 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult email_put_result,
                              document_store->Put(email_document));
-  EXPECT_FALSE(email_put_result.was_replacement);
+  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)));
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult message_put_result,
                              document_store->Put(message_document));
-  EXPECT_FALSE(message_put_result.was_replacement);
+  EXPECT_THAT(message_put_result.old_document_id, Eq(kInvalidDocumentId));
+  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)));
@@ -3117,7 +3380,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
   ICING_EXPECT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/false));
@@ -3149,22 +3413,28 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult email_put_result,
                              document_store->Put(email_document));
-  EXPECT_FALSE(email_put_result.was_replacement);
+  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;
   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));
-  EXPECT_FALSE(message_put_result.was_replacement);
+  EXPECT_THAT(message_put_result.old_document_id, Eq(kInvalidDocumentId));
+  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));
 
   // Rearrange the schema types. Since SchemaTypeId is assigned based on order,
@@ -3197,12 +3467,16 @@
       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));
 }
 
@@ -3223,7 +3497,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
   ICING_EXPECT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/false));
@@ -3255,7 +3530,9 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_without_subject_put_result,
       document_store->Put(email_without_subject));
-  EXPECT_FALSE(email_without_subject_put_result.was_replacement);
+  EXPECT_THAT(email_without_subject_put_result.old_document_id,
+              Eq(kInvalidDocumentId));
+  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),
@@ -3264,7 +3541,9 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::PutResult email_with_subject_put_result,
       document_store->Put(email_with_subject));
-  EXPECT_FALSE(email_with_subject_put_result.was_replacement);
+  EXPECT_THAT(email_with_subject_put_result.old_document_id,
+              Eq(kInvalidDocumentId));
+  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),
@@ -3308,7 +3587,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
   ICING_EXPECT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
       /*allow_circular_schema_definitions=*/false));
@@ -3338,14 +3618,16 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult email_put_result,
                              document_store->Put(email_document));
-  EXPECT_FALSE(email_put_result.was_replacement);
+  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)));
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult message_put_result,
                              document_store->Put(message_document));
-  EXPECT_FALSE(message_put_result.was_replacement);
+  EXPECT_THAT(message_put_result.old_document_id, Eq(kInvalidDocumentId));
+  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)));
@@ -3509,7 +3791,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
                              document_store->Put(test_document1_));
-  EXPECT_FALSE(put_result1.was_replacement);
+  EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result1.was_replacement());
   DocumentId document_id = put_result1.new_document_id;
 
   // Report usage with type 1 and time 1.
@@ -3603,7 +3886,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
                              document_store->Put(test_document1_));
-  EXPECT_FALSE(put_result1.was_replacement);
+  EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result1.was_replacement());
   DocumentId document_id = put_result1.new_document_id;
 
   // Report usage with type 1.
@@ -3658,7 +3942,8 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
                                document_store->Put(test_document1_));
-    EXPECT_FALSE(put_result1.was_replacement);
+    EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
+    EXPECT_FALSE(put_result1.was_replacement());
     document_id = put_result1.new_document_id;
 
     // Report usage with type 1.
@@ -3705,7 +3990,8 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
                                document_store->Put(test_document1_));
-    EXPECT_FALSE(put_result1.was_replacement);
+    EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
+    EXPECT_FALSE(put_result1.was_replacement());
     document_id = put_result1.new_document_id;
 
     // Report usage with type 1.
@@ -3760,7 +4046,8 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
                              document_store->Put(test_document1_));
-  EXPECT_FALSE(put_result1.was_replacement);
+  EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result1.was_replacement());
   DocumentId document_id = put_result1.new_document_id;
 
   // Report usage with type 1.
@@ -3780,7 +4067,8 @@
   // Update the document.
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
                              document_store->Put(test_document1_));
-  EXPECT_TRUE(put_result2.was_replacement);
+  EXPECT_THAT(put_result2.old_document_id, Eq(document_id));
+  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)));
@@ -3803,11 +4091,13 @@
 
   ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
                              document_store->Put(test_document1_));
-  EXPECT_FALSE(put_result1.was_replacement);
+  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,
                              document_store->Put(test_document2_));
-  EXPECT_FALSE(put_result2.was_replacement);
+  EXPECT_THAT(put_result2.old_document_id, Eq(kInvalidDocumentId));
+  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()));
@@ -3863,7 +4153,8 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
                                doc_store->Put(test_document1_));
-    EXPECT_FALSE(put_result.was_replacement);
+    EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+    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_)));
@@ -3918,7 +4209,8 @@
 
     ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
                                doc_store->Put(test_document1_));
-    EXPECT_FALSE(put_result.was_replacement);
+    EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+    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_)));
@@ -4004,10 +4296,10 @@
       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().namespace_id_fingerprint, GetParam().pre_mapping_fbv,
-          GetParam().use_persistent_hash_map,
-          PortableFileBackedProtoLog<DocumentWrapper>::kDeflateCompressionLevel,
+          GetParam().pre_mapping_fbv, GetParam().use_persistent_hash_map,
+          PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
           &initialize_stats));
   std::unique_ptr<DocumentStore> doc_store =
       std::move(create_result.document_store);
@@ -4153,7 +4445,8 @@
   SchemaProto schema = SchemaBuilder().AddType(email_type_config).Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ASSERT_THAT(schema_store->SetSchema(
                   schema, /*ignore_errors_and_delete_documents=*/false,
                   /*allow_circular_schema_definitions=*/false),
@@ -4184,13 +4477,16 @@
             .Build();
     ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
                                doc_store->Put(doc));
-    EXPECT_FALSE(put_result.was_replacement);
+    EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+    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));
   }
 
@@ -4224,14 +4520,14 @@
     InitializeStatsProto initialize_stats;
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(
-            &filesystem_, document_store_dir_, &fake_clock_, schema_store.get(),
-            /*force_recovery_and_revalidate_documents=*/true,
-            GetParam().namespace_id_fingerprint, GetParam().pre_mapping_fbv,
-            GetParam().use_persistent_hash_map,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            &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,
+                              &initialize_stats));
     std::unique_ptr<DocumentStore> doc_store =
         std::move(create_result.document_store);
 
@@ -4240,6 +4536,8 @@
         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(initialize_stats.document_store_recovery_cause(),
                 Eq(InitializeStatsProto::SCHEMA_CHANGES_OUT_OF_SYNC));
@@ -4268,7 +4566,8 @@
   SchemaProto schema = SchemaBuilder().AddType(email_type_config).Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ASSERT_THAT(schema_store->SetSchema(
                   schema, /*ignore_errors_and_delete_documents=*/false,
                   /*allow_circular_schema_definitions=*/false),
@@ -4299,13 +4598,16 @@
             .Build();
     ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
                                doc_store->Put(doc));
-    EXPECT_FALSE(put_result.was_replacement);
+    EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+    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));
   }
 
@@ -4348,6 +4650,8 @@
         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));
   }
 }
@@ -4374,7 +4678,8 @@
   SchemaProto schema = SchemaBuilder().AddType(email_type_config).Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ASSERT_THAT(schema_store->SetSchema(
                   schema, /*ignore_errors_and_delete_documents=*/false,
                   /*allow_circular_schema_definitions=*/false),
@@ -4415,13 +4720,16 @@
     DocumentId docid = kInvalidDocumentId;
     ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_with_body_result,
                                doc_store->Put(docWithBody));
-    EXPECT_FALSE(put_with_body_result.was_replacement);
+    EXPECT_THAT(put_with_body_result.old_document_id, Eq(kInvalidDocumentId));
+    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));
-    EXPECT_FALSE(put_without_body_result.was_replacement);
+    EXPECT_THAT(put_without_body_result.old_document_id,
+                Eq(kInvalidDocumentId));
+    EXPECT_FALSE(put_without_body_result.was_replacement());
     docid = put_without_body_result.new_document_id;
     ASSERT_NE(docid, kInvalidDocumentId);
 
@@ -4453,14 +4761,14 @@
     CorruptDocStoreHeaderChecksumFile();
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(
-            &filesystem_, document_store_dir_, &fake_clock_, schema_store.get(),
-            /*force_recovery_and_revalidate_documents=*/true,
-            GetParam().namespace_id_fingerprint, GetParam().pre_mapping_fbv,
-            GetParam().use_persistent_hash_map,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     std::unique_ptr<DocumentStore> doc_store =
         std::move(create_result.document_store);
 
@@ -4494,7 +4802,8 @@
   SchemaProto schema = SchemaBuilder().AddType(email_type_config).Build();
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+      SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_,
+                          feature_flags_.get()));
   ASSERT_THAT(schema_store->SetSchema(
                   schema, /*ignore_errors_and_delete_documents=*/false,
                   /*allow_circular_schema_definitions=*/false),
@@ -4535,13 +4844,16 @@
     DocumentId docid = kInvalidDocumentId;
     ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_with_body_result,
                                doc_store->Put(docWithBody));
-    EXPECT_FALSE(put_with_body_result.was_replacement);
+    EXPECT_THAT(put_with_body_result.old_document_id, Eq(kInvalidDocumentId));
+    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));
-    EXPECT_FALSE(put_without_body_result.was_replacement);
+    EXPECT_THAT(put_without_body_result.old_document_id,
+                Eq(kInvalidDocumentId));
+    EXPECT_FALSE(put_without_body_result.was_replacement());
     docid = put_without_body_result.new_document_id;
     ASSERT_NE(docid, kInvalidDocumentId);
 
@@ -4610,7 +4922,8 @@
   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_));
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
 
   ASSERT_THAT(schema_store->SetSchema(
                   schema, /*ignore_errors_and_delete_documents=*/false,
@@ -4654,10 +4967,10 @@
       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,
-          GetParam().namespace_id_fingerprint,
-          PortableFileBackedProtoLog<DocumentWrapper>::kDeflateCompressionLevel,
+          PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
           &initialize_stats));
   std::unique_ptr<DocumentStore> document_store =
       std::move(create_result.document_store);
@@ -4742,7 +5055,8 @@
   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_));
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
 
   ICING_ASSERT_OK(schema_store->SetSchema(
       schema, /*ignore_errors_and_delete_documents=*/false,
@@ -4847,7 +5161,8 @@
   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_));
+      SchemaStore::Create(&filesystem_, schema_store_dir, &fake_clock_,
+                          feature_flags_.get()));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       DocumentStore::CreateResult create_result,
@@ -4892,20 +5207,20 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
         DocumentStore::Create(&filesystem_, document_store_dir_, &fake_clock_,
-                              schema_store_.get(),
+                              schema_store_.get(), feature_flags_.get(),
                               /*force_recovery_and_revalidate_documents=*/false,
-                              GetParam().namespace_id_fingerprint,
                               GetParam().pre_mapping_fbv,
                               GetParam().use_persistent_hash_map,
                               PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDeflateCompressionLevel,
+                                  DocumentWrapper>::kDefaultCompressionLevel,
                               /*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_));
-    EXPECT_FALSE(put_result1.was_replacement);
+    EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
+    EXPECT_FALSE(put_result1.was_replacement());
     document_id1 = put_result1.new_document_id;
 
     if (GetParam().use_persistent_hash_map) {
@@ -4934,12 +5249,12 @@
         DocumentStore::CreateResult create_result,
         DocumentStore::Create(
             &filesystem_, document_store_dir_, &fake_clock_,
-            schema_store_.get(),
+            schema_store_.get(), feature_flags_.get(),
             /*force_recovery_and_revalidate_documents=*/false,
-            GetParam().namespace_id_fingerprint, GetParam().pre_mapping_fbv,
+            GetParam().pre_mapping_fbv,
             /*use_persistent_hash_map=*/switch_key_mapper_flag,
             PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
+                DocumentWrapper>::kDefaultCompressionLevel,
             &initialize_stats));
     EXPECT_THAT(initialize_stats.document_store_recovery_cause(),
                 Eq(InitializeStatsProto::IO_ERROR));
@@ -4978,20 +5293,20 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
         DocumentStore::Create(&filesystem_, document_store_dir_, &fake_clock_,
-                              schema_store_.get(),
+                              schema_store_.get(), feature_flags_.get(),
                               /*force_recovery_and_revalidate_documents=*/false,
-                              GetParam().namespace_id_fingerprint,
                               GetParam().pre_mapping_fbv,
                               GetParam().use_persistent_hash_map,
                               PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDeflateCompressionLevel,
+                                  DocumentWrapper>::kDefaultCompressionLevel,
                               /*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_));
-    EXPECT_FALSE(put_result1.was_replacement);
+    EXPECT_THAT(put_result1.old_document_id, Eq(kInvalidDocumentId));
+    EXPECT_FALSE(put_result1.was_replacement());
     document_id1 = put_result1.new_document_id;
 
     if (GetParam().use_persistent_hash_map) {
@@ -5017,13 +5332,12 @@
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
         DocumentStore::Create(&filesystem_, document_store_dir_, &fake_clock_,
-                              schema_store_.get(),
+                              schema_store_.get(), feature_flags_.get(),
                               /*force_recovery_and_revalidate_documents=*/false,
-                              GetParam().namespace_id_fingerprint,
                               GetParam().pre_mapping_fbv,
                               GetParam().use_persistent_hash_map,
                               PortableFileBackedProtoLog<
-                                  DocumentWrapper>::kDeflateCompressionLevel,
+                                  DocumentWrapper>::kDefaultCompressionLevel,
                               &initialize_stats));
     EXPECT_THAT(initialize_stats.document_store_recovery_cause(),
                 Eq(InitializeStatsProto::NONE));
@@ -5052,7 +5366,7 @@
   }
 }
 
-TEST_P(DocumentStoreTest, GetDocumentIdByNamespaceFingerprintIdentifier) {
+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 =
@@ -5061,65 +5375,657 @@
       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().namespace_id_fingerprint, GetParam().pre_mapping_fbv,
-          GetParam().use_persistent_hash_map,
-          PortableFileBackedProtoLog<DocumentWrapper>::kDeflateCompressionLevel,
+          GetParam().pre_mapping_fbv, GetParam().use_persistent_hash_map,
+          PortableFileBackedProtoLog<DocumentWrapper>::kDefaultCompressionLevel,
           /*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_));
-  EXPECT_FALSE(put_result.was_replacement);
+  EXPECT_THAT(put_result.old_document_id, Eq(kInvalidDocumentId));
+  EXPECT_FALSE(put_result.was_replacement());
   DocumentId document_id = put_result.new_document_id;
 
   ICING_ASSERT_OK_AND_ASSIGN(
       NamespaceId namespace_id,
       doc_store->GetNamespaceId(test_document1_.namespace_()));
-  NamespaceFingerprintIdentifier ns_fingerprint(
-      namespace_id,
-      /*target_str=*/test_document1_.uri());
-  if (GetParam().namespace_id_fingerprint) {
-    EXPECT_THAT(doc_store->GetDocumentId(ns_fingerprint),
-                IsOkAndHolds(document_id));
+  NamespaceIdFingerprint nsid_uri_fingerprint(
+      namespace_id, /*target_str=*/test_document1_.uri());
+  EXPECT_THAT(doc_store->GetDocumentId(nsid_uri_fingerprint),
+              IsOkAndHolds(document_id));
 
-    NamespaceFingerprintIdentifier non_existing_ns_fingerprint(
-        namespace_id + 1, /*target_str=*/test_document1_.uri());
-    EXPECT_THAT(doc_store->GetDocumentId(non_existing_ns_fingerprint),
-                StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
-  } else {
-    EXPECT_THAT(doc_store->GetDocumentId(ns_fingerprint),
-                StatusIs(libtextclassifier3::StatusCode::FAILED_PRECONDITION));
-  }
+  NamespaceIdFingerprint non_existing_nsid_uri_fingerprint(
+      namespace_id + 1, /*target_str=*/test_document1_.uri());
+  EXPECT_THAT(doc_store->GetDocumentId(non_existing_nsid_uri_fingerprint),
+              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
+}
+
+TEST_P(DocumentStoreTest, PutDocumentWithNoScorablePropertiesInSchema) {
+  const std::string schema_store_dir = test_dir_ + "_custom";
+  filesystem_.DeleteDirectoryRecursively(schema_store_dir.c_str());
+  filesystem_.CreateDirectoryRecursively(schema_store_dir.c_str());
+
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("message").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("subject")
+                  .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                  .SetCardinality(CARDINALITY_OPTIONAL)))
+          .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,
+      /*allow_circular_schema_definitions=*/false));
+
+  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);
+
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("foo", "1")
+                               .SetSchema("message")
+                               .AddStringProperty("subject", "subject foo")
+                               .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             doc_store->Put(document));
+  DocumentId document_id = put_result.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentAssociatedScoreData score_data,
+      doc_store->GetDocumentAssociatedScoreData(document_id));
+  EXPECT_EQ(score_data.scorable_property_cache_index(), -1);
+  EXPECT_EQ(doc_store->GetScorablePropertySet(
+                document_id, fake_clock_.GetSystemTimeMilliseconds()),
+            nullptr);
+}
+
+TEST_P(DocumentStoreTest, PutDocumentWithNoScorableProperties) {
+  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);
+
+  DocumentProto document = DocumentBuilder()
+                               .SetKey("icing", "email/1")
+                               .SetSchema("email")
+                               .AddStringProperty("subject", "subject foo")
+                               .Build();
+
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             doc_store->Put(document));
+  DocumentId document_id = put_result.new_document_id;
+  std::unique_ptr<ScorablePropertySet> scorable_property_set =
+      doc_store->GetScorablePropertySet(
+          document_id, fake_clock_.GetSystemTimeMilliseconds());
+  // scorable property data for the document exists but is empty.
+  EXPECT_THAT(scorable_property_set->GetScorablePropertyProto("score"),
+              Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({}))));
+}
+
+TEST_P(DocumentStoreTest, PutDocumentWithScorablePropertyThenRead) {
+  const std::string schema_store_dir = test_dir_ + "_custom";
+  filesystem_.DeleteDirectoryRecursively(schema_store_dir.c_str());
+  filesystem_.CreateDirectoryRecursively(schema_store_dir.c_str());
+
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("importance")
+                          .SetDataType(PropertyConfigProto::DataType::BOOLEAN)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("scoreDouble")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("scoreInt64")
+                                   .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                   .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                   .SetCardinality(CARDINALITY_REPEATED)))
+          .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,
+      /*allow_circular_schema_definitions=*/false));
+
+  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);
+
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("foo", "1")
+                                .SetSchema("email")
+                                .AddStringProperty("subject", "subject foo")
+                                .AddBooleanProperty("importance", true)
+                                .AddInt64Property("scoreInt64", 1)
+                                .AddDoubleProperty("scoreDouble", 1.5, 2.5)
+                                .SetCreationTimestampMs(0)
+                                .Build();
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("bar", "2")
+                                .SetSchema("email")
+                                .AddStringProperty("subject", "subject bar")
+                                .AddBooleanProperty("importance", false)
+                                .AddInt64Property("scoreInt64", 5)
+                                .SetCreationTimestampMs(0)
+                                .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                             doc_store->Put(document1));
+  DocumentId document_id1 = put_result1.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result2,
+                             doc_store->Put(document2));
+  DocumentId document_id2 = put_result2.new_document_id;
+
+  std::unique_ptr<ScorablePropertySet> scorable_property_set_doc1 =
+      doc_store->GetScorablePropertySet(
+          document_id1, fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(
+      scorable_property_set_doc1->GetScorablePropertyProto("importance"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromBoolean(true))));
+  EXPECT_THAT(
+      scorable_property_set_doc1->GetScorablePropertyProto("scoreInt64"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromInt64({1}))));
+  EXPECT_THAT(
+      scorable_property_set_doc1->GetScorablePropertyProto("scoreDouble"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({1.5, 2.5}))));
+
+  std::unique_ptr<ScorablePropertySet> scorable_property_set_doc2 =
+      doc_store->GetScorablePropertySet(
+          document_id2, fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(
+      scorable_property_set_doc2->GetScorablePropertyProto("importance"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromBoolean(false))));
+  EXPECT_THAT(
+      scorable_property_set_doc2->GetScorablePropertyProto("scoreInt64"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromInt64({5}))));
+  EXPECT_THAT(
+      scorable_property_set_doc2->GetScorablePropertyProto("scoreDouble"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({}))));
+
+  // document3 is a copy of document1 with scorable property scoreDouble
+  // updated.
+  DocumentProto document3 = DocumentBuilder()
+                                .SetKey("foo", "1")
+                                .SetSchema("email")
+                                .AddStringProperty("subject", "subject foo")
+                                .AddBooleanProperty("importance", true)
+                                .AddInt64Property("scoreInt64", 1)
+                                .AddDoubleProperty("scoreDouble", 0.5, 0.8)
+                                .SetCreationTimestampMs(0)
+                                .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));
+  DocumentId document_id3 = put_result3.new_document_id;
+  std::unique_ptr<ScorablePropertySet> scorable_property_set_doc3 =
+      doc_store->GetScorablePropertySet(
+          document_id3, fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(
+      scorable_property_set_doc3->GetScorablePropertyProto("importance"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromBoolean(true))));
+
+  EXPECT_THAT(
+      scorable_property_set_doc3->GetScorablePropertyProto("scoreInt64"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromInt64({1}))));
+  EXPECT_THAT(
+      scorable_property_set_doc3->GetScorablePropertyProto("scoreDouble"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({0.5, 0.8}))));
+
+  // document4 is a copy of document3 with scorable property scoreInt64
+  // removed.
+  DocumentProto document4 = DocumentBuilder()
+                                .SetKey("foo", "1")
+                                .SetSchema("email")
+                                .AddStringProperty("subject", "subject foo")
+                                .AddBooleanProperty("importance", true)
+                                .AddDoubleProperty("scoreDouble", 0.5, 0.8)
+                                .SetCreationTimestampMs(0)
+                                .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));
+  DocumentId document_id4 = put_result4.new_document_id;
+  std::unique_ptr<ScorablePropertySet> scorable_property_set_doc4 =
+      doc_store->GetScorablePropertySet(
+          document_id4, fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(
+      scorable_property_set_doc4->GetScorablePropertyProto("importance"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromBoolean(true))));
+  EXPECT_THAT(
+      scorable_property_set_doc4->GetScorablePropertyProto("scoreInt64"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromInt64({}))));
+  EXPECT_THAT(
+      scorable_property_set_doc4->GetScorablePropertyProto("scoreDouble"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({0.5, 0.8}))));
+}
+
+TEST_P(DocumentStoreTest, ReadScorablePropertyAfterOptimization) {
+  const std::string schema_store_dir = test_dir_ + "_custom";
+  filesystem_.DeleteDirectoryRecursively(schema_store_dir.c_str());
+  filesystem_.CreateDirectoryRecursively(schema_store_dir.c_str());
+
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(
+              SchemaTypeConfigBuilder()
+                  .SetType("email")
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("subject")
+                          .SetDataTypeString(TERM_MATCH_EXACT, TOKENIZER_PLAIN)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("importance")
+                          .SetDataType(PropertyConfigProto::DataType::BOOLEAN)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_OPTIONAL))
+                  .AddProperty(
+                      PropertyConfigBuilder()
+                          .SetName("scoreDouble")
+                          .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                          .SetScorableType(SCORABLE_TYPE_ENABLED)
+                          .SetCardinality(CARDINALITY_REPEATED))
+                  .AddProperty(PropertyConfigBuilder()
+                                   .SetName("scoreInt64")
+                                   .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                   .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                   .SetCardinality(CARDINALITY_REPEATED)))
+          .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,
+      /*allow_circular_schema_definitions=*/false));
+
+  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);
+
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("foo", "1")
+                                .SetSchema("email")
+                                .AddStringProperty("subject", "subject foo")
+                                .AddBooleanProperty("importance", true)
+                                .AddInt64Property("scoreInt64", 1)
+                                .AddDoubleProperty("scoreDouble", 1.5, 2.5)
+                                .SetCreationTimestampMs(0)
+                                .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result1,
+                             doc_store->Put(document1));
+  DocumentId document_id1 = put_result1.new_document_id;
+
+  std::unique_ptr<ScorablePropertySet> scorable_property_set_doc1 =
+      doc_store->GetScorablePropertySet(
+          document_id1, fake_clock_.GetSystemTimeMilliseconds());
+
+  EXPECT_THAT(
+      scorable_property_set_doc1->GetScorablePropertyProto("importance"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromBoolean(true))));
+  EXPECT_THAT(
+      scorable_property_set_doc1->GetScorablePropertyProto("scoreInt64"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromInt64({1}))));
+  EXPECT_THAT(
+      scorable_property_set_doc1->GetScorablePropertyProto("scoreDouble"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({1.5, 2.5}))));
+
+  // document2 is a copy of document1 with scorable property scoreDouble
+  // updated.
+  DocumentProto document2 = DocumentBuilder()
+                                .SetKey("foo", "1")
+                                .SetSchema("email")
+                                .AddStringProperty("subject", "subject foo")
+                                .AddBooleanProperty("importance", true)
+                                .AddInt64Property("scoreInt64", 1)
+                                .AddDoubleProperty("scoreDouble", 0.5, 0.8)
+                                .SetCreationTimestampMs(0)
+                                .Build();
+
+  // 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));
+  DocumentId document_id2 = put_result2.new_document_id;
+  std::unique_ptr<ScorablePropertySet> scorable_property_set_doc2 =
+      doc_store->GetScorablePropertySet(
+          document_id2, fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(
+      scorable_property_set_doc2->GetScorablePropertyProto("importance"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromBoolean(true))));
+  EXPECT_THAT(
+      scorable_property_set_doc2->GetScorablePropertyProto("scoreInt64"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromInt64({1}))));
+  EXPECT_THAT(
+      scorable_property_set_doc2->GetScorablePropertyProto("scoreDouble"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({0.5, 0.8}))));
+
+  // Optimize the document store.
+  std::string optimized_dir = document_store_dir_ + "_optimize";
+  ASSERT_TRUE(filesystem_.DeleteDirectoryRecursively(optimized_dir.c_str()));
+  ASSERT_TRUE(filesystem_.CreateDirectoryRecursively(optimized_dir.c_str()));
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentStore::OptimizeResult optimize_result,
+      doc_store->OptimizeInto(
+          optimized_dir, lang_segmenter_.get(),
+          /*expired_blob_handles=*/std::unordered_set<std::string>()));
+
+  // Verify that the scorable property set is still correct after optimization.
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentId doc_id_post_optimization,
+                             doc_store->GetDocumentId("foo", "1"));
+  std::unique_ptr<ScorablePropertySet> scorable_property_set_post_optimization =
+      doc_store->GetScorablePropertySet(
+          doc_id_post_optimization, fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(
+      scorable_property_set_post_optimization->GetScorablePropertyProto(
+          "importance"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromBoolean(true))));
+  EXPECT_THAT(scorable_property_set_post_optimization->GetScorablePropertyProto(
+                  "scoreInt64"),
+              Pointee(EqualsProto(BuildScorablePropertyProtoFromInt64({1}))));
+  EXPECT_THAT(
+      scorable_property_set_post_optimization->GetScorablePropertyProto(
+          "scoreDouble"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({0.5, 0.8}))));
+}
+
+TEST_P(DocumentStoreTest,
+       RegenerateScorablePropertyCacheFlipPropertyToScorableEnabled) {
+  const std::string schema_store_dir = test_dir_ + "_custom";
+  filesystem_.DeleteDirectoryRecursively(schema_store_dir.c_str());
+  filesystem_.CreateDirectoryRecursively(schema_store_dir.c_str());
+
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("income")
+                  .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                  .SetCardinality(CARDINALITY_REPEATED)))
+          .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,
+      /*allow_circular_schema_definitions=*/false));
+  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);
+
+  DocumentProto document0 = DocumentBuilder()
+                                .SetKey("icing", "person0")
+                                .SetSchema("Person")
+                                .SetScore(10)
+                                .SetCreationTimestampMs(1)
+                                .AddDoubleProperty("income", 10000, 20000)
+                                .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             doc_store->Put(document0));
+  DocumentId document_id = put_result.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentAssociatedScoreData score_data,
+      doc_store->GetDocumentAssociatedScoreData(document_id));
+  EXPECT_EQ(score_data.scorable_property_cache_index(), -1);
+  EXPECT_EQ(doc_store->GetScorablePropertySet(
+                document_id, fake_clock_.GetSystemTimeMilliseconds()),
+            nullptr);
+
+  // Update the schema to make "income" property scorable.
+  schema = SchemaBuilder()
+               .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+                   PropertyConfigBuilder()
+                       .SetName("income")
+                       .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                       .SetScorableType(SCORABLE_TYPE_ENABLED)
+                       .SetCardinality(CARDINALITY_REPEATED)))
+               .Build();
+  ICING_EXPECT_OK(schema_store->SetSchema(
+      schema, /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/false));
+  ICING_EXPECT_OK(doc_store->UpdateSchemaStore(schema_store.get()));
+  ICING_ASSERT_OK_AND_ASSIGN(const SchemaTypeId schema_type_id,
+                             schema_store->GetSchemaTypeId("Person"));
+  ICING_EXPECT_OK(doc_store->RegenerateScorablePropertyCache({schema_type_id}));
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      score_data, doc_store->GetDocumentAssociatedScoreData(document_id));
+  EXPECT_NE(score_data.scorable_property_cache_index(), -1);
+  std::unique_ptr<ScorablePropertySet> scorable_property_set =
+      doc_store->GetScorablePropertySet(
+          document_id, fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(scorable_property_set->GetScorablePropertyProto("income"),
+              Pointee(EqualsProto(
+                  BuildScorablePropertyProtoFromDouble({10000, 20000}))));
+}
+
+TEST_P(DocumentStoreTest,
+       RegenerateScorablePropertyCacheFlipPropertyToScorableDisabled) {
+  const std::string schema_store_dir = test_dir_ + "_custom";
+  filesystem_.DeleteDirectoryRecursively(schema_store_dir.c_str());
+  filesystem_.CreateDirectoryRecursively(schema_store_dir.c_str());
+
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("income")
+                  .SetScorableType(SCORABLE_TYPE_ENABLED)
+                  .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                  .SetCardinality(CARDINALITY_REPEATED)))
+          .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,
+      /*allow_circular_schema_definitions=*/false));
+  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);
+
+  DocumentProto document0 = DocumentBuilder()
+                                .SetKey("icing", "person0")
+                                .SetSchema("Person")
+                                .SetScore(10)
+                                .SetCreationTimestampMs(1)
+                                .AddDoubleProperty("income", 10000, 20000)
+                                .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             doc_store->Put(document0));
+  DocumentId document_id = put_result.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(
+      DocumentAssociatedScoreData score_data,
+      doc_store->GetDocumentAssociatedScoreData(document_id));
+  EXPECT_NE(score_data.scorable_property_cache_index(), -1);
+  std::unique_ptr<ScorablePropertySet> scorable_property_set =
+      doc_store->GetScorablePropertySet(
+          document_id, fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(scorable_property_set->GetScorablePropertyProto("income"),
+              Pointee(EqualsProto(
+                  BuildScorablePropertyProtoFromDouble({10000, 20000}))));
+
+  // Update the schema to make "income" property non-scorable.
+  schema = SchemaBuilder()
+               .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+                   PropertyConfigBuilder()
+                       .SetName("income")
+                       .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                       .SetCardinality(CARDINALITY_REPEATED)))
+               .Build();
+  ICING_EXPECT_OK(schema_store->SetSchema(
+      schema, /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/false));
+  ICING_EXPECT_OK(doc_store->UpdateSchemaStore(schema_store.get()));
+  ICING_ASSERT_OK_AND_ASSIGN(const SchemaTypeId schema_type_id,
+                             schema_store->GetSchemaTypeId("Person"));
+  ICING_EXPECT_OK(doc_store->RegenerateScorablePropertyCache({schema_type_id}));
+  EXPECT_EQ(doc_store->GetScorablePropertySet(
+                document_id, fake_clock_.GetSystemTimeMilliseconds()),
+            nullptr);
+  ICING_ASSERT_OK_AND_ASSIGN(
+      score_data, doc_store->GetDocumentAssociatedScoreData(document_id));
+  EXPECT_EQ(score_data.scorable_property_cache_index(), -1);
+}
+
+TEST_P(DocumentStoreTest,
+       RegenerateScorablePropertyCacheWithSchemaTypeIdChange) {
+  const std::string schema_store_dir = test_dir_ + "_custom";
+  filesystem_.DeleteDirectoryRecursively(schema_store_dir.c_str());
+  filesystem_.CreateDirectoryRecursively(schema_store_dir.c_str());
+
+  SchemaProto schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("income")
+                  .SetScorableType(SCORABLE_TYPE_ENABLED)
+                  .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                  .SetCardinality(CARDINALITY_REPEATED)))
+          .AddType(SchemaTypeConfigBuilder().SetType("Message").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("score")
+                  .SetScorableType(SCORABLE_TYPE_ENABLED)
+                  .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                  .SetCardinality(CARDINALITY_REPEATED)))
+          .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,
+      /*allow_circular_schema_definitions=*/false));
+  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);
+
+  DocumentProto document0 = DocumentBuilder()
+                                .SetKey("icing", "person0")
+                                .SetSchema("Person")
+                                .SetScore(10)
+                                .SetCreationTimestampMs(1)
+                                .AddDoubleProperty("income", 10000, 20000)
+                                .Build();
+  DocumentProto document1 = DocumentBuilder()
+                                .SetKey("icing", "message0")
+                                .SetSchema("Message")
+                                .SetScore(10)
+                                .SetCreationTimestampMs(1)
+                                .AddDoubleProperty("score", 10, 20)
+                                .Build();
+  ICING_ASSERT_OK_AND_ASSIGN(DocumentStore::PutResult put_result,
+                             doc_store->Put(document0));
+  DocumentId document_id0 = put_result.new_document_id;
+  ICING_ASSERT_OK_AND_ASSIGN(put_result, doc_store->Put(document1));
+  DocumentId document_id1 = put_result.new_document_id;
+
+  // Update the schema by rearranging the schema types. Since SchemaTypeId is
+  // assigned based on order, this should change the SchemaTypeIds.
+  schema =
+      SchemaBuilder()
+          .AddType(SchemaTypeConfigBuilder().SetType("Message").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("score")
+                  .SetScorableType(SCORABLE_TYPE_ENABLED)
+                  .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                  .SetCardinality(CARDINALITY_REPEATED)))
+          .AddType(SchemaTypeConfigBuilder().SetType("Person").AddProperty(
+              PropertyConfigBuilder()
+                  .SetName("income")
+                  .SetScorableType(SCORABLE_TYPE_ENABLED)
+                  .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                  .SetCardinality(CARDINALITY_REPEATED)))
+          .Build();
+
+  ICING_EXPECT_OK(schema_store->SetSchema(
+      schema, /*ignore_errors_and_delete_documents=*/false,
+      /*allow_circular_schema_definitions=*/false));
+  ICING_EXPECT_OK(doc_store->UpdateSchemaStore(schema_store.get()));
+  ICING_ASSERT_OK_AND_ASSIGN(const SchemaTypeId person_schema_type_id,
+                             schema_store->GetSchemaTypeId("Person"));
+  ICING_ASSERT_OK_AND_ASSIGN(const SchemaTypeId message_schema_type_id,
+                             schema_store->GetSchemaTypeId("Message"));
+  ICING_EXPECT_OK(doc_store->RegenerateScorablePropertyCache(
+      {person_schema_type_id, message_schema_type_id}));
+
+  std::unique_ptr<ScorablePropertySet> scorable_property_set0 =
+      doc_store->GetScorablePropertySet(
+          document_id0, fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(scorable_property_set0->GetScorablePropertyProto("income"),
+              Pointee(EqualsProto(
+                  BuildScorablePropertyProtoFromDouble({10000, 20000}))));
+  std::unique_ptr<ScorablePropertySet> scorable_property_set1 =
+      doc_store->GetScorablePropertySet(
+          document_id1, fake_clock_.GetSystemTimeMilliseconds());
+  EXPECT_THAT(
+      scorable_property_set1->GetScorablePropertyProto("score"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({10, 20}))));
 }
 
 INSTANTIATE_TEST_SUITE_P(
     DocumentStoreTest, DocumentStoreTest,
     testing::Values(
-        DocumentStoreTestParam(/*namespace_id_fingerprint_in=*/false,
-                               /*pre_mapping_fbv_in=*/false,
+        DocumentStoreTestParam(/*pre_mapping_fbv_in=*/false,
                                /*use_persistent_hash_map_in=*/false),
-        DocumentStoreTestParam(/*namespace_id_fingerprint_in=*/true,
-                               /*pre_mapping_fbv_in=*/false,
-                               /*use_persistent_hash_map_in=*/false),
-        DocumentStoreTestParam(/*namespace_id_fingerprint_in=*/false,
-                               /*pre_mapping_fbv_in=*/true,
-                               /*use_persistent_hash_map_in=*/false),
-        DocumentStoreTestParam(/*namespace_id_fingerprint_in=*/true,
-                               /*pre_mapping_fbv_in=*/true,
-                               /*use_persistent_hash_map_in=*/false),
-        DocumentStoreTestParam(/*namespace_id_fingerprint_in=*/false,
-                               /*pre_mapping_fbv_in=*/false,
+        DocumentStoreTestParam(/*pre_mapping_fbv_in=*/false,
                                /*use_persistent_hash_map_in=*/true),
-        DocumentStoreTestParam(/*namespace_id_fingerprint_in=*/true,
-                               /*pre_mapping_fbv_in=*/false,
-                               /*use_persistent_hash_map_in=*/true),
-        DocumentStoreTestParam(/*namespace_id_fingerprint_in=*/false,
-                               /*pre_mapping_fbv_in=*/true,
-                               /*use_persistent_hash_map_in=*/true),
-        DocumentStoreTestParam(/*namespace_id_fingerprint_in=*/true,
-                               /*pre_mapping_fbv_in=*/true,
+        DocumentStoreTestParam(/*pre_mapping_fbv_in=*/true,
+                               /*use_persistent_hash_map_in=*/false),
+        DocumentStoreTestParam(/*pre_mapping_fbv_in=*/true,
                                /*use_persistent_hash_map_in=*/true)));
 
 }  // namespace
diff --git a/icing/store/key-mapper_test.cc b/icing/store/key-mapper_test.cc
index fa7d1e8..229e519 100644
--- a/icing/store/key-mapper_test.cc
+++ b/icing/store/key-mapper_test.cc
@@ -224,6 +224,122 @@
       UnorderedElementsAre(Pair("foo", 1), Pair("bar", 2), Pair("baz", 3)));
 }
 
+TEST_P(KeyMapperTest, DeleteByKey_singleEntry) {
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<KeyMapper<DocumentId>> key_mapper,
+                             CreateKeyMapper());
+  EXPECT_THAT(GetAllKeyValuePairs(key_mapper.get()), IsEmpty());
+
+  ICING_ASSERT_OK(key_mapper->Put("foo", /*value=*/1));
+  ASSERT_THAT(key_mapper->num_keys(), 1);
+  ASSERT_THAT(GetAllKeyValuePairs(key_mapper.get()),
+              UnorderedElementsAre(Pair("foo", 1)));
+
+  // Delete "foo".
+  EXPECT_THAT(key_mapper->Delete("foo"), IsTrue());
+
+  // Verify num_keys().
+  EXPECT_THAT(key_mapper->num_keys(), 0);
+
+  // Verify Get() returns NOT_FOUND.
+  EXPECT_THAT(key_mapper->Get("foo"),
+              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
+
+  // Verify the iterator returns nothing.
+  EXPECT_THAT(GetAllKeyValuePairs(key_mapper.get()), IsEmpty());
+
+  // Add back "foo" a with different value and verify the key mapper.
+  EXPECT_THAT(key_mapper->Put("foo", /*value=*/12345), IsOk());
+  EXPECT_THAT(key_mapper->num_keys(), 1);
+  EXPECT_THAT(key_mapper->Get("foo"), IsOkAndHolds(12345));
+  EXPECT_THAT(GetAllKeyValuePairs(key_mapper.get()),
+              UnorderedElementsAre(Pair("foo", 12345)));
+}
+
+TEST_P(KeyMapperTest, DeleteByKey_multipleEntries) {
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<KeyMapper<DocumentId>> key_mapper,
+                             CreateKeyMapper());
+  EXPECT_THAT(GetAllKeyValuePairs(key_mapper.get()), IsEmpty());
+
+  ICING_ASSERT_OK(key_mapper->Put("foo", /*value=*/1));
+  ICING_ASSERT_OK(key_mapper->Put("bar", /*value=*/2));
+  ICING_ASSERT_OK(key_mapper->Put("baz", /*value=*/3));
+  ASSERT_THAT(key_mapper->num_keys(), 3);
+  ASSERT_THAT(
+      GetAllKeyValuePairs(key_mapper.get()),
+      UnorderedElementsAre(Pair("foo", 1), Pair("bar", 2), Pair("baz", 3)));
+
+  // Delete "foo".
+  EXPECT_THAT(key_mapper->Delete("foo"), IsTrue());
+
+  // Verify num_keys().
+  EXPECT_THAT(key_mapper->num_keys(), 2);
+
+  // Verify Get("foo") returns NOT_FOUND and Get("bar") and Get("baz") return
+  // the correct values.
+  EXPECT_THAT(key_mapper->Get("foo"),
+              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
+  EXPECT_THAT(key_mapper->Get("bar"), IsOkAndHolds(2));
+  EXPECT_THAT(key_mapper->Get("baz"), IsOkAndHolds(3));
+
+  // Verify the iterator returns "bar" and "baz".
+  EXPECT_THAT(GetAllKeyValuePairs(key_mapper.get()),
+              UnorderedElementsAre(Pair("bar", 2), Pair("baz", 3)));
+
+  // Add back "foo" a with different value and verify the key mapper.
+  EXPECT_THAT(key_mapper->Put("foo", /*value=*/12345), IsOk());
+  EXPECT_THAT(key_mapper->num_keys(), 3);
+  EXPECT_THAT(key_mapper->Get("foo"), IsOkAndHolds(12345));
+  EXPECT_THAT(key_mapper->Get("bar"), IsOkAndHolds(2));
+  EXPECT_THAT(key_mapper->Get("baz"), IsOkAndHolds(3));
+  EXPECT_THAT(
+      GetAllKeyValuePairs(key_mapper.get()),
+      UnorderedElementsAre(Pair("foo", 12345), Pair("bar", 2), Pair("baz", 3)));
+}
+
+TEST_P(KeyMapperTest, DeleteByKey_multipleEntriesDeleteAll) {
+  ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<KeyMapper<DocumentId>> key_mapper,
+                             CreateKeyMapper());
+  EXPECT_THAT(GetAllKeyValuePairs(key_mapper.get()), IsEmpty());
+
+  ICING_ASSERT_OK(key_mapper->Put("foo", /*value=*/1));
+  ICING_ASSERT_OK(key_mapper->Put("bar", /*value=*/2));
+  ICING_ASSERT_OK(key_mapper->Put("baz", /*value=*/3));
+  ASSERT_THAT(key_mapper->num_keys(), 3);
+  ASSERT_THAT(
+      GetAllKeyValuePairs(key_mapper.get()),
+      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());
+
+  // Verify num_keys().
+  EXPECT_THAT(key_mapper->num_keys(), 0);
+
+  // Verify Get("foo"), Get("bar") and Get("baz") return NOT_FOUND.
+  EXPECT_THAT(key_mapper->Get("foo"),
+              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
+  EXPECT_THAT(key_mapper->Get("bar"),
+              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
+  EXPECT_THAT(key_mapper->Get("baz"),
+              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
+
+  // Verify the iterator returns nothing.
+  EXPECT_THAT(GetAllKeyValuePairs(key_mapper.get()), IsEmpty());
+
+  // Add back "foo" a with different value and verify the key mapper.
+  EXPECT_THAT(key_mapper->Put("foo", /*value=*/12345), IsOk());
+  EXPECT_THAT(key_mapper->num_keys(), 1);
+  EXPECT_THAT(key_mapper->Get("foo"), IsOkAndHolds(12345));
+  EXPECT_THAT(key_mapper->Get("bar"),
+              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
+  EXPECT_THAT(key_mapper->Get("baz"),
+              StatusIs(libtextclassifier3::StatusCode::NOT_FOUND));
+  EXPECT_THAT(GetAllKeyValuePairs(key_mapper.get()),
+              UnorderedElementsAre(Pair("foo", 12345)));
+}
+
 INSTANTIATE_TEST_SUITE_P(
     KeyMapperTest, KeyMapperTest,
     testing::Values(KeyMapperTestParam(KeyMapperType::kDynamicTrie,
diff --git a/icing/store/namespace-fingerprint-identifier.h b/icing/store/namespace-fingerprint-identifier.h
deleted file mode 100644
index d91ef94..0000000
--- a/icing/store/namespace-fingerprint-identifier.h
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright (C) 2023 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_STORE_NAMESPACE_FINGERPRINT_IDENTIFIER_H_
-#define ICING_STORE_NAMESPACE_FINGERPRINT_IDENTIFIER_H_
-
-#include <cstdint>
-#include <string>
-#include <string_view>
-
-#include "icing/text_classifier/lib3/utils/base/statusor.h"
-#include "icing/store/namespace-id.h"
-
-namespace icing {
-namespace lib {
-
-class NamespaceFingerprintIdentifier {
- public:
-  static constexpr int kEncodedNamespaceIdLength = 3;
-  static constexpr int kMinEncodedLength = kEncodedNamespaceIdLength + 1;
-
-  static libtextclassifier3::StatusOr<NamespaceFingerprintIdentifier>
-  DecodeFromCString(std::string_view encoded_cstr);
-
-  explicit NamespaceFingerprintIdentifier()
-      : namespace_id_(0), fingerprint_(0) {}
-
-  explicit NamespaceFingerprintIdentifier(NamespaceId namespace_id,
-                                          uint64_t fingerprint)
-      : namespace_id_(namespace_id), fingerprint_(fingerprint) {}
-
-  explicit NamespaceFingerprintIdentifier(NamespaceId namespace_id,
-                                          std::string_view target_str);
-
-  std::string EncodeToCString() const;
-
-  bool operator<(const NamespaceFingerprintIdentifier& other) const {
-    if (namespace_id_ != other.namespace_id_) {
-      return namespace_id_ < other.namespace_id_;
-    }
-    return fingerprint_ < other.fingerprint_;
-  }
-
-  bool operator==(const NamespaceFingerprintIdentifier& other) const {
-    return namespace_id_ == other.namespace_id_ &&
-           fingerprint_ == other.fingerprint_;
-  }
-
-  NamespaceId namespace_id() const { return namespace_id_; }
-  uint64_t fingerprint() const { return fingerprint_; }
-
- private:
-  NamespaceId namespace_id_;
-  uint64_t fingerprint_;
-} __attribute__((packed));
-static_assert(sizeof(NamespaceFingerprintIdentifier) == 10, "");
-
-}  // namespace lib
-}  // namespace icing
-
-#endif  // ICING_STORE_NAMESPACE_FINGERPRINT_IDENTIFIER_H_
diff --git a/icing/store/namespace-fingerprint-identifier_test.cc b/icing/store/namespace-fingerprint-identifier_test.cc
deleted file mode 100644
index 5f86156..0000000
--- a/icing/store/namespace-fingerprint-identifier_test.cc
+++ /dev/null
@@ -1,148 +0,0 @@
-// Copyright (C) 2023 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/store/namespace-fingerprint-identifier.h"
-
-#include <cstdint>
-#include <limits>
-#include <string>
-
-#include "icing/text_classifier/lib3/utils/base/status.h"
-#include "gmock/gmock.h"
-#include "gtest/gtest.h"
-#include "icing/store/namespace-id.h"
-#include "icing/testing/common-matchers.h"
-
-namespace icing {
-namespace lib {
-
-namespace {
-
-using ::testing::Eq;
-
-TEST(NamespaceFingerprintIdentifierTest, EncodeToCString) {
-  NamespaceFingerprintIdentifier identifier1(/*namespace_id=*/0,
-                                             /*fingerprint=*/0);
-  EXPECT_THAT(identifier1.EncodeToCString(), Eq("\x01\x01\x01\x01"));
-
-  NamespaceFingerprintIdentifier identifier2(/*namespace_id=*/0,
-                                             /*fingerprint=*/1);
-  EXPECT_THAT(identifier2.EncodeToCString(), Eq("\x01\x01\x01\x02"));
-
-  NamespaceFingerprintIdentifier identifier3(
-      /*namespace_id=*/0, /*fingerprint=*/std::numeric_limits<uint64_t>::max());
-  EXPECT_THAT(identifier3.EncodeToCString(),
-              Eq("\x01\x01\x01\x80\x80\x80\x80\x80\x80\x80\x80\x80\x02"));
-
-  NamespaceFingerprintIdentifier identifier4(/*namespace_id=*/1,
-                                             /*fingerprint=*/0);
-  EXPECT_THAT(identifier4.EncodeToCString(), Eq("\x02\x01\x01\x01"));
-
-  NamespaceFingerprintIdentifier identifier5(/*namespace_id=*/1,
-                                             /*fingerprint=*/1);
-  EXPECT_THAT(identifier5.EncodeToCString(), Eq("\x02\x01\x01\x02"));
-
-  NamespaceFingerprintIdentifier identifier6(
-      /*namespace_id=*/1, /*fingerprint=*/std::numeric_limits<uint64_t>::max());
-  EXPECT_THAT(identifier6.EncodeToCString(),
-              Eq("\x02\x01\x01\x80\x80\x80\x80\x80\x80\x80\x80\x80\x02"));
-
-  NamespaceFingerprintIdentifier identifier7(
-      /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
-      /*fingerprint=*/0);
-  EXPECT_THAT(identifier7.EncodeToCString(), Eq("\x80\x80\x02\x01"));
-
-  NamespaceFingerprintIdentifier identifier8(
-      /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
-      /*fingerprint=*/1);
-  EXPECT_THAT(identifier8.EncodeToCString(), Eq("\x80\x80\x02\x02"));
-
-  NamespaceFingerprintIdentifier identifier9(
-      /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
-      /*fingerprint=*/std::numeric_limits<uint64_t>::max());
-  EXPECT_THAT(identifier9.EncodeToCString(),
-              Eq("\x80\x80\x02\x80\x80\x80\x80\x80\x80\x80\x80\x80\x02"));
-}
-
-TEST(NamespaceFingerprintIdentifierTest,
-     MultipleCStringConversionsAreReversible) {
-  NamespaceFingerprintIdentifier identifier1(/*namespace_id=*/0,
-                                             /*fingerprint=*/0);
-  EXPECT_THAT(NamespaceFingerprintIdentifier::DecodeFromCString(
-                  identifier1.EncodeToCString()),
-              IsOkAndHolds(identifier1));
-
-  NamespaceFingerprintIdentifier identifier2(/*namespace_id=*/0,
-                                             /*fingerprint=*/1);
-  EXPECT_THAT(NamespaceFingerprintIdentifier::DecodeFromCString(
-                  identifier2.EncodeToCString()),
-              IsOkAndHolds(identifier2));
-
-  NamespaceFingerprintIdentifier identifier3(
-      /*namespace_id=*/0, /*fingerprint=*/std::numeric_limits<uint64_t>::max());
-  EXPECT_THAT(NamespaceFingerprintIdentifier::DecodeFromCString(
-                  identifier3.EncodeToCString()),
-              IsOkAndHolds(identifier3));
-
-  NamespaceFingerprintIdentifier identifier4(/*namespace_id=*/1,
-                                             /*fingerprint=*/0);
-  EXPECT_THAT(NamespaceFingerprintIdentifier::DecodeFromCString(
-                  identifier4.EncodeToCString()),
-              IsOkAndHolds(identifier4));
-
-  NamespaceFingerprintIdentifier identifier5(/*namespace_id=*/1,
-                                             /*fingerprint=*/1);
-  EXPECT_THAT(NamespaceFingerprintIdentifier::DecodeFromCString(
-                  identifier5.EncodeToCString()),
-              IsOkAndHolds(identifier5));
-
-  NamespaceFingerprintIdentifier identifier6(
-      /*namespace_id=*/1, /*fingerprint=*/std::numeric_limits<uint64_t>::max());
-  EXPECT_THAT(NamespaceFingerprintIdentifier::DecodeFromCString(
-                  identifier6.EncodeToCString()),
-              IsOkAndHolds(identifier6));
-
-  NamespaceFingerprintIdentifier identifier7(
-      /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
-      /*fingerprint=*/0);
-  EXPECT_THAT(NamespaceFingerprintIdentifier::DecodeFromCString(
-                  identifier7.EncodeToCString()),
-              IsOkAndHolds(identifier7));
-
-  NamespaceFingerprintIdentifier identifier8(
-      /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
-      /*fingerprint=*/1);
-  EXPECT_THAT(NamespaceFingerprintIdentifier::DecodeFromCString(
-                  identifier8.EncodeToCString()),
-              IsOkAndHolds(identifier8));
-
-  NamespaceFingerprintIdentifier identifier9(
-      /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
-      /*fingerprint=*/std::numeric_limits<uint64_t>::max());
-  EXPECT_THAT(NamespaceFingerprintIdentifier::DecodeFromCString(
-                  identifier9.EncodeToCString()),
-              IsOkAndHolds(identifier9));
-}
-
-TEST(NamespaceFingerprintIdentifierTest,
-     DecodeFromCStringInvalidLengthShouldReturnError) {
-  std::string invalid_str = "\x01\x01\x01";
-  EXPECT_THAT(NamespaceFingerprintIdentifier::DecodeFromCString(invalid_str),
-              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
-}
-
-}  // namespace
-
-}  // namespace lib
-}  // namespace icing
diff --git a/icing/store/namespace-fingerprint-identifier.cc b/icing/store/namespace-id-fingerprint.cc
similarity index 83%
rename from icing/store/namespace-fingerprint-identifier.cc
rename to icing/store/namespace-id-fingerprint.cc
index 3910105..9caafde 100644
--- a/icing/store/namespace-fingerprint-identifier.cc
+++ b/icing/store/namespace-id-fingerprint.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "icing/store/namespace-fingerprint-identifier.h"
+#include "icing/store/namespace-id-fingerprint.h"
 
 #include <cstdint>
 #include <string>
@@ -28,9 +28,8 @@
 namespace icing {
 namespace lib {
 
-/* static */ libtextclassifier3::StatusOr<NamespaceFingerprintIdentifier>
-NamespaceFingerprintIdentifier::DecodeFromCString(
-    std::string_view encoded_cstr) {
+/* static */ libtextclassifier3::StatusOr<NamespaceIdFingerprint>
+NamespaceIdFingerprint::DecodeFromCString(std::string_view encoded_cstr) {
   if (encoded_cstr.size() < kMinEncodedLength) {
     return absl_ports::InvalidArgumentError("Invalid length");
   }
@@ -39,15 +38,15 @@
       encoded_cstr.substr(0, kEncodedNamespaceIdLength));
   uint64_t fingerprint = encode_util::DecodeIntFromCString(
       encoded_cstr.substr(kEncodedNamespaceIdLength));
-  return NamespaceFingerprintIdentifier(namespace_id, fingerprint);
+  return NamespaceIdFingerprint(namespace_id, fingerprint);
 }
 
-NamespaceFingerprintIdentifier::NamespaceFingerprintIdentifier(
-    NamespaceId namespace_id, std::string_view target_str)
+NamespaceIdFingerprint::NamespaceIdFingerprint(NamespaceId namespace_id,
+                                               std::string_view target_str)
     : namespace_id_(namespace_id),
       fingerprint_(tc3farmhash::Fingerprint64(target_str)) {}
 
-std::string NamespaceFingerprintIdentifier::EncodeToCString() const {
+std::string NamespaceIdFingerprint::EncodeToCString() const {
   // encoded_namespace_id_str should be 1 to 3 bytes based on the value of
   // namespace_id.
   std::string encoded_namespace_id_str =
diff --git a/icing/store/namespace-id-fingerprint.h b/icing/store/namespace-id-fingerprint.h
new file mode 100644
index 0000000..bae294e
--- /dev/null
+++ b/icing/store/namespace-id-fingerprint.h
@@ -0,0 +1,86 @@
+// Copyright (C) 2023 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_STORE_NAMESPACE_ID_FINGERPRINT_H_
+#define ICING_STORE_NAMESPACE_ID_FINGERPRINT_H_
+
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+#include <string>
+#include <string_view>
+
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/store/namespace-id.h"
+
+namespace icing {
+namespace lib {
+
+// A wrapper class of namespace id and fingerprint. It is usually used as a
+// unique identifier of a resource, e.g. document (namespace id +
+// fingerprint(uri)), corpus (namespace id + fingerprint(schema_type_name)).
+class NamespaceIdFingerprint {
+ public:
+  struct Hasher {
+    std::size_t operator()(const NamespaceIdFingerprint& nsid_fp) const {
+      return std::hash<NamespaceId>()(nsid_fp.namespace_id_) ^
+             std::hash<uint64_t>()(nsid_fp.fingerprint_);
+    }
+  };
+
+  static constexpr int kEncodedNamespaceIdLength = 3;
+  static constexpr int kMinEncodedLength = kEncodedNamespaceIdLength + 1;
+
+  static libtextclassifier3::StatusOr<NamespaceIdFingerprint> DecodeFromCString(
+      std::string_view encoded_cstr);
+
+  explicit NamespaceIdFingerprint()
+      : namespace_id_(kInvalidNamespaceId), fingerprint_(0) {}
+
+  explicit NamespaceIdFingerprint(NamespaceId namespace_id,
+                                  uint64_t fingerprint)
+      : namespace_id_(namespace_id), fingerprint_(fingerprint) {}
+
+  explicit NamespaceIdFingerprint(NamespaceId namespace_id,
+                                  std::string_view target_str);
+
+  std::string EncodeToCString() const;
+
+  bool is_valid() const { return namespace_id_ >= 0; }
+
+  bool operator<(const NamespaceIdFingerprint& other) const {
+    if (namespace_id_ != other.namespace_id_) {
+      return namespace_id_ < other.namespace_id_;
+    }
+    return fingerprint_ < other.fingerprint_;
+  }
+
+  bool operator==(const NamespaceIdFingerprint& other) const {
+    return namespace_id_ == other.namespace_id_ &&
+           fingerprint_ == other.fingerprint_;
+  }
+
+  NamespaceId namespace_id() const { return namespace_id_; }
+  uint64_t fingerprint() const { return fingerprint_; }
+
+ private:
+  NamespaceId namespace_id_;
+  uint64_t fingerprint_;
+} __attribute__((packed));
+static_assert(sizeof(NamespaceIdFingerprint) == 10, "");
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_STORE_NAMESPACE_ID_FINGERPRINT_H_
diff --git a/icing/store/namespace-id-fingerprint_test.cc b/icing/store/namespace-id-fingerprint_test.cc
new file mode 100644
index 0000000..c11ed64
--- /dev/null
+++ b/icing/store/namespace-id-fingerprint_test.cc
@@ -0,0 +1,199 @@
+// Copyright (C) 2023 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/store/namespace-id-fingerprint.h"
+
+#include <cstdint>
+#include <limits>
+#include <string>
+
+#include "icing/text_classifier/lib3/utils/base/status.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "icing/store/namespace-id.h"
+#include "icing/testing/common-matchers.h"
+
+namespace icing {
+namespace lib {
+
+namespace {
+
+using ::testing::Eq;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
+
+TEST(NamespaceIdFingerprintTest, Invalid) {
+  NamespaceIdFingerprint identifier1(/*namespace_id=*/-1, /*fingerprint=*/0);
+  EXPECT_THAT(identifier1.is_valid(), IsFalse());
+
+  NamespaceIdFingerprint identifier2(/*namespace_id=*/-1, /*fingerprint=*/1);
+  EXPECT_THAT(identifier2.is_valid(), IsFalse());
+
+  NamespaceIdFingerprint identifier3(/*namespace_id=*/-2, /*fingerprint=*/1);
+  EXPECT_THAT(identifier3.is_valid(), IsFalse());
+
+  NamespaceIdFingerprint identifier4(
+      /*namespace_id=*/std::numeric_limits<NamespaceId>::min(),
+      /*fingerprint=*/1);
+  EXPECT_THAT(identifier4.is_valid(), IsFalse());
+}
+
+TEST(NamespaceIdFingerprintTest, DefaultInvalid) {
+  NamespaceIdFingerprint identifier;
+  EXPECT_THAT(identifier.is_valid(), IsFalse());
+}
+
+TEST(NamespaceIdFingerprintTest, Valid) {
+  NamespaceIdFingerprint identifier1(/*namespace_id=*/0, /*fingerprint=*/0);
+  EXPECT_THAT(identifier1.is_valid(), IsTrue());
+
+  NamespaceIdFingerprint identifier2(/*namespace_id=*/0, /*fingerprint=*/1);
+  EXPECT_THAT(identifier2.is_valid(), IsTrue());
+
+  NamespaceIdFingerprint identifier3(
+      /*namespace_id=*/0, /*fingerprint=*/std::numeric_limits<uint64_t>::max());
+  EXPECT_THAT(identifier3.is_valid(), IsTrue());
+
+  NamespaceIdFingerprint identifier4(/*namespace_id=*/1, /*fingerprint=*/0);
+  EXPECT_THAT(identifier4.is_valid(), IsTrue());
+
+  NamespaceIdFingerprint identifier5(/*namespace_id=*/1, /*fingerprint=*/1);
+  EXPECT_THAT(identifier5.is_valid(), IsTrue());
+
+  NamespaceIdFingerprint identifier6(
+      /*namespace_id=*/1, /*fingerprint=*/std::numeric_limits<uint64_t>::max());
+  EXPECT_THAT(identifier6.is_valid(), IsTrue());
+
+  NamespaceIdFingerprint identifier7(
+      /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
+      /*fingerprint=*/0);
+  EXPECT_THAT(identifier7.is_valid(), IsTrue());
+
+  NamespaceIdFingerprint identifier8(
+      /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
+      /*fingerprint=*/1);
+  EXPECT_THAT(identifier8.is_valid(), IsTrue());
+
+  NamespaceIdFingerprint identifier9(
+      /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
+      /*fingerprint=*/std::numeric_limits<uint64_t>::max());
+  EXPECT_THAT(identifier9.is_valid(), IsTrue());
+}
+
+TEST(NamespaceIdFingerprintTest, EncodeToCString) {
+  NamespaceIdFingerprint identifier1(/*namespace_id=*/0, /*fingerprint=*/0);
+  EXPECT_THAT(identifier1.EncodeToCString(), Eq("\x01\x01\x01\x01"));
+
+  NamespaceIdFingerprint identifier2(/*namespace_id=*/0, /*fingerprint=*/1);
+  EXPECT_THAT(identifier2.EncodeToCString(), Eq("\x01\x01\x01\x02"));
+
+  NamespaceIdFingerprint identifier3(
+      /*namespace_id=*/0, /*fingerprint=*/std::numeric_limits<uint64_t>::max());
+  EXPECT_THAT(identifier3.EncodeToCString(),
+              Eq("\x01\x01\x01\x80\x80\x80\x80\x80\x80\x80\x80\x80\x02"));
+
+  NamespaceIdFingerprint identifier4(/*namespace_id=*/1, /*fingerprint=*/0);
+  EXPECT_THAT(identifier4.EncodeToCString(), Eq("\x02\x01\x01\x01"));
+
+  NamespaceIdFingerprint identifier5(/*namespace_id=*/1, /*fingerprint=*/1);
+  EXPECT_THAT(identifier5.EncodeToCString(), Eq("\x02\x01\x01\x02"));
+
+  NamespaceIdFingerprint identifier6(
+      /*namespace_id=*/1, /*fingerprint=*/std::numeric_limits<uint64_t>::max());
+  EXPECT_THAT(identifier6.EncodeToCString(),
+              Eq("\x02\x01\x01\x80\x80\x80\x80\x80\x80\x80\x80\x80\x02"));
+
+  NamespaceIdFingerprint identifier7(
+      /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
+      /*fingerprint=*/0);
+  EXPECT_THAT(identifier7.EncodeToCString(), Eq("\x80\x80\x02\x01"));
+
+  NamespaceIdFingerprint identifier8(
+      /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
+      /*fingerprint=*/1);
+  EXPECT_THAT(identifier8.EncodeToCString(), Eq("\x80\x80\x02\x02"));
+
+  NamespaceIdFingerprint identifier9(
+      /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
+      /*fingerprint=*/std::numeric_limits<uint64_t>::max());
+  EXPECT_THAT(identifier9.EncodeToCString(),
+              Eq("\x80\x80\x02\x80\x80\x80\x80\x80\x80\x80\x80\x80\x02"));
+}
+
+TEST(NamespaceIdFingerprintTest, MultipleCStringConversionsAreReversible) {
+  NamespaceIdFingerprint identifier1(/*namespace_id=*/0, /*fingerprint=*/0);
+  EXPECT_THAT(
+      NamespaceIdFingerprint::DecodeFromCString(identifier1.EncodeToCString()),
+      IsOkAndHolds(identifier1));
+
+  NamespaceIdFingerprint identifier2(/*namespace_id=*/0, /*fingerprint=*/1);
+  EXPECT_THAT(
+      NamespaceIdFingerprint::DecodeFromCString(identifier2.EncodeToCString()),
+      IsOkAndHolds(identifier2));
+
+  NamespaceIdFingerprint identifier3(
+      /*namespace_id=*/0, /*fingerprint=*/std::numeric_limits<uint64_t>::max());
+  EXPECT_THAT(
+      NamespaceIdFingerprint::DecodeFromCString(identifier3.EncodeToCString()),
+      IsOkAndHolds(identifier3));
+
+  NamespaceIdFingerprint identifier4(/*namespace_id=*/1, /*fingerprint=*/0);
+  EXPECT_THAT(
+      NamespaceIdFingerprint::DecodeFromCString(identifier4.EncodeToCString()),
+      IsOkAndHolds(identifier4));
+
+  NamespaceIdFingerprint identifier5(/*namespace_id=*/1, /*fingerprint=*/1);
+  EXPECT_THAT(
+      NamespaceIdFingerprint::DecodeFromCString(identifier5.EncodeToCString()),
+      IsOkAndHolds(identifier5));
+
+  NamespaceIdFingerprint identifier6(
+      /*namespace_id=*/1, /*fingerprint=*/std::numeric_limits<uint64_t>::max());
+  EXPECT_THAT(
+      NamespaceIdFingerprint::DecodeFromCString(identifier6.EncodeToCString()),
+      IsOkAndHolds(identifier6));
+
+  NamespaceIdFingerprint identifier7(
+      /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
+      /*fingerprint=*/0);
+  EXPECT_THAT(
+      NamespaceIdFingerprint::DecodeFromCString(identifier7.EncodeToCString()),
+      IsOkAndHolds(identifier7));
+
+  NamespaceIdFingerprint identifier8(
+      /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
+      /*fingerprint=*/1);
+  EXPECT_THAT(
+      NamespaceIdFingerprint::DecodeFromCString(identifier8.EncodeToCString()),
+      IsOkAndHolds(identifier8));
+
+  NamespaceIdFingerprint identifier9(
+      /*namespace_id=*/std::numeric_limits<NamespaceId>::max(),
+      /*fingerprint=*/std::numeric_limits<uint64_t>::max());
+  EXPECT_THAT(
+      NamespaceIdFingerprint::DecodeFromCString(identifier9.EncodeToCString()),
+      IsOkAndHolds(identifier9));
+}
+
+TEST(NamespaceIdFingerprintTest,
+     DecodeFromCStringInvalidLengthShouldReturnError) {
+  std::string invalid_str = "\x01\x01\x01";
+  EXPECT_THAT(NamespaceIdFingerprint::DecodeFromCString(invalid_str),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/testing/common-matchers.h b/icing/testing/common-matchers.h
index fd117b8..aaa4f68 100644
--- a/icing/testing/common-matchers.h
+++ b/icing/testing/common-matchers.h
@@ -37,6 +37,7 @@
 #include "icing/proto/status.pb.h"
 #include "icing/schema/joinable-property.h"
 #include "icing/schema/schema-store.h"
+#include "icing/schema/scorable_property_manager.h"
 #include "icing/schema/section.h"
 #include "icing/scoring/scored-document-hit.h"
 
@@ -55,6 +56,17 @@
   return true;
 }
 
+MATCHER_P(EqualsNormalizedTerm, text, "") {
+  std::string arg_string(arg.text.data(), arg.text.length());
+  if (arg.text != text) {
+    *result_listener << IcingStringUtil::StringPrintf(
+        "(Expected: text=\"%s\". Actual: text=\"%s\")", text,
+        arg_string.c_str());
+    return false;
+  }
+  return true;
+}
+
 // Used to match a DocHitInfo
 MATCHER_P2(EqualsDocHitInfo, document_id, section_ids, "") {
   const DocHitInfo& actual = arg;
@@ -105,6 +117,26 @@
          actual.num_blocks_inspected == num_blocks_inspected;
 }
 
+// Used to match a DocumentAssociatedScoreData
+MATCHER_P5(EqualsDocumentAssociatedScoreData, corpus_id, document_score,
+           creation_timestamp_ms, length_in_tokens,
+           has_valid_scorable_property_cache_index, "") {
+  bool expected_has_valid_scorable_property_cache_index =
+      arg.scorable_property_cache_index() != -1;
+  return arg.corpus_id() == corpus_id &&
+         arg.document_score() == document_score &&
+         arg.creation_timestamp_ms() == creation_timestamp_ms &&
+         arg.length_in_tokens() == length_in_tokens &&
+         expected_has_valid_scorable_property_cache_index ==
+             has_valid_scorable_property_cache_index;
+}
+
+// Used to match a ScorablePropertyManager::ScorablePropertyInfo
+MATCHER_P2(EqualsScorablePropertyInfo, property_path, data_type, "") {
+  const ScorablePropertyManager::ScorablePropertyInfo& actual = arg;
+  return actual.property_path == property_path && actual.data_type == data_type;
+}
+
 struct ExtractTermFrequenciesResult {
   std::array<Hit::TermFrequency, kTotalNumSections> term_frequencies = {0};
   SectionIdMask section_mask = kSectionIdMaskNone;
@@ -288,7 +320,9 @@
       actual.schema_types_index_incompatible_by_name ==
           expected.schema_types_index_incompatible_by_name &&
       actual.schema_types_join_incompatible_by_name ==
-          expected.schema_types_join_incompatible_by_name) {
+          expected.schema_types_join_incompatible_by_name &&
+      actual.schema_types_scorable_property_inconsistent_by_id ==
+          expected.schema_types_scorable_property_inconsistent_by_id) {
     return true;
   }
 
@@ -459,7 +493,13 @@
                  .term_match_type() &&
          actual.numeric_match_type ==
              expected_property_config_proto.integer_indexing_config()
-                 .numeric_match_type();
+                 .numeric_match_type() &&
+         actual.embedding_indexing_type ==
+             expected_property_config_proto.embedding_indexing_config()
+                 .embedding_indexing_type() &&
+         actual.quantization_type ==
+             expected_property_config_proto.embedding_indexing_config()
+                 .quantization_type();
 }
 
 MATCHER_P3(EqualsJoinablePropertyMetadata, expected_id, expected_property_path,
@@ -468,7 +508,10 @@
   return actual.id == expected_id && actual.path == expected_property_path &&
          actual.data_type == expected_property_config_proto.data_type() &&
          actual.value_type ==
-             expected_property_config_proto.joinable_config().value_type();
+             expected_property_config_proto.joinable_config().value_type() &&
+         actual.delete_propagation_type ==
+             expected_property_config_proto.joinable_config()
+                 .delete_propagation_type();
 }
 
 std::string StatusCodeToString(libtextclassifier3::StatusCode code);
diff --git a/icing/testing/embedding-test-utils.cc b/icing/testing/embedding-test-utils.cc
new file mode 100644
index 0000000..23f4197
--- /dev/null
+++ b/icing/testing/embedding-test-utils.cc
@@ -0,0 +1,87 @@
+// Copyright (C) 2024 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/testing/embedding-test-utils.h"
+
+#include <cstdint>
+#include <memory>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#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/posting-list-embedding-hit-accessor.h"
+#include "icing/index/embed/quantizer.h"
+#include "icing/proto/document.pb.h"
+#include "icing/util/status-macros.h"
+
+namespace icing {
+namespace lib {
+
+libtextclassifier3::StatusOr<std::vector<EmbeddingHit>>
+GetEmbeddingHitsFromIndex(const EmbeddingIndex* embedding_index,
+                          uint32_t dimension,
+                          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())) {
+    return hits;
+  }
+  ICING_ASSIGN_OR_RETURN(
+      std::unique_ptr<PostingListEmbeddingHitAccessor> pl_accessor,
+      std::move(pl_accessor_or));
+
+  while (true) {
+    ICING_ASSIGN_OR_RETURN(std::vector<EmbeddingHit> batch,
+                           pl_accessor->GetNextHitsBatch());
+    if (batch.empty()) {
+      return hits;
+    }
+    hits.insert(hits.end(), batch.begin(), batch.end());
+  }
+}
+
+std::vector<float> GetRawEmbeddingDataFromIndex(
+    const EmbeddingIndex* embedding_index) {
+  ICING_ASSIGN_OR_RETURN(const float* data,
+                         embedding_index->GetRawEmbeddingData(),
+                         std::vector<float>());
+  return std::vector<float>(data, data + embedding_index->GetTotalVectorSize());
+}
+
+libtextclassifier3::StatusOr<std::vector<float>>
+GetAndRestoreQuantizedEmbeddingVectorFromIndex(
+    const EmbeddingIndex* embedding_index, const EmbeddingHit& hit,
+    uint32_t dimension) {
+  ICING_ASSIGN_OR_RETURN(
+      const char* data,
+      embedding_index->GetQuantizedEmbeddingVector(hit, dimension));
+  Quantizer quantizer(data);
+  const uint8_t* quantized_vector =
+      reinterpret_cast<const uint8_t*>(data + sizeof(Quantizer));
+  std::vector<float> result;
+  result.reserve(dimension);
+  for (int i = 0; i < dimension; ++i) {
+    result.push_back(quantizer.Dequantize(quantized_vector[i]));
+  }
+  return result;
+}
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/testing/embedding-test-utils.h b/icing/testing/embedding-test-utils.h
index 931953e..7f0b0c1 100644
--- a/icing/testing/embedding-test-utils.h
+++ b/icing/testing/embedding-test-utils.h
@@ -15,9 +15,15 @@
 #ifndef ICING_TESTING_EMBEDDING_TEST_UTILS_H_
 #define ICING_TESTING_EMBEDDING_TEST_UTILS_H_
 
+#include <cstdint>
 #include <initializer_list>
 #include <string>
+#include <string_view>
+#include <vector>
 
+#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/proto/document.pb.h"
 
 namespace icing {
@@ -39,6 +45,20 @@
   return CreateVector(model_signature, values...);
 }
 
+libtextclassifier3::StatusOr<std::vector<EmbeddingHit>>
+GetEmbeddingHitsFromIndex(const EmbeddingIndex* embedding_index,
+                          uint32_t dimension, std::string_view model_signature);
+
+std::vector<float> GetRawEmbeddingDataFromIndex(
+    const EmbeddingIndex* embedding_index);
+
+// 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);
+
 }  // namespace lib
 }  // namespace icing
 
diff --git a/icing/testing/iterator-test-utils.cc b/icing/testing/iterator-test-utils.cc
new file mode 100644
index 0000000..0999530
--- /dev/null
+++ b/icing/testing/iterator-test-utils.cc
@@ -0,0 +1,96 @@
+// Copyright (C) 2024 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/testing/iterator-test-utils.h"
+
+#include <string_view>
+#include <vector>
+
+#include "icing/tokenization/language-segmenter.h"
+#include "icing/util/character-iterator.h"
+
+namespace icing {
+namespace lib {
+
+// Returns a vector containing all terms retrieved by Advancing on the iterator.
+std::vector<std::string_view> GetAllTermsAdvance(
+    LanguageSegmenter::Iterator* itr) {
+  std::vector<std::string_view> terms;
+  while (itr->Advance()) {
+    terms.push_back(itr->GetTerm());
+  }
+  return terms;
+}
+
+// Returns a vector containing all terms retrieved by calling ResetAfter with
+// the UTF-32 position of the current term start to simulate Advancing on the
+// iterator.
+std::vector<std::string_view> GetAllTermsResetAfterUtf32(
+    LanguageSegmenter::Iterator* itr) {
+  std::vector<std::string_view> terms;
+  // Calling ResetToTermStartingAfterUtf32 with -1 should get the first term in
+  // the sequence.
+  bool is_ok = itr->ResetToTermStartingAfterUtf32(-1).ok();
+  while (is_ok) {
+    terms.push_back(itr->GetTerm());
+    // Calling ResetToTermStartingAfterUtf32 with the current position should
+    // get the very next term in the sequence.
+    CharacterIterator char_itr = itr->CalculateTermStart().ValueOrDie();
+    is_ok = itr->ResetToTermStartingAfterUtf32(char_itr.utf32_index()).ok();
+  }
+  return terms;
+}
+
+// Returns a vector containing all terms retrieved by alternating calls to
+// Advance and calls to ResetAfter with the UTF-32 position of the current term
+// start to simulate Advancing.
+std::vector<std::string_view> GetAllTermsAdvanceAndResetAfterUtf32(
+    LanguageSegmenter::Iterator* itr) {
+  std::vector<std::string_view> terms;
+  bool is_ok = itr->Advance();
+  while (is_ok) {
+    terms.push_back(itr->GetTerm());
+    // Alternate between using Advance and ResetToTermAfter.
+    if (terms.size() % 2 == 0) {
+      is_ok = itr->Advance();
+    } else {
+      // Calling ResetToTermStartingAfterUtf32 with the current position should
+      // get the very next term in the sequence.
+      CharacterIterator char_itr = itr->CalculateTermStart().ValueOrDie();
+      is_ok = itr->ResetToTermStartingAfterUtf32(char_itr.utf32_index()).ok();
+    }
+  }
+  return terms;
+}
+
+// Returns a vector containing all terms retrieved by calling ResetBefore with
+// the UTF-32 position of the current term start, starting at the end of the
+// text. This vector should be in reverse order of GetAllTerms and missing the
+// last term.
+std::vector<std::string_view> GetAllTermsResetBeforeUtf32(
+    LanguageSegmenter::Iterator* itr) {
+  std::vector<std::string_view> terms;
+  bool is_ok = itr->ResetToTermEndingBeforeUtf32(1000).ok();
+  while (is_ok) {
+    terms.push_back(itr->GetTerm());
+    // Calling ResetToTermEndingBeforeUtf32 with the current position should get
+    // the previous term in the sequence.
+    CharacterIterator char_itr = itr->CalculateTermStart().ValueOrDie();
+    is_ok = itr->ResetToTermEndingBeforeUtf32(char_itr.utf32_index()).ok();
+  }
+  return terms;
+}
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/testing/iterator-test-utils.h b/icing/testing/iterator-test-utils.h
new file mode 100644
index 0000000..089143f
--- /dev/null
+++ b/icing/testing/iterator-test-utils.h
@@ -0,0 +1,52 @@
+// Copyright (C) 2024 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_TESTING_ITERATOR_TEST_UTILS_H_
+#define ICING_TESTING_ITERATOR_TEST_UTILS_H_
+
+#include <string_view>
+#include <vector>
+
+#include "icing/tokenization/language-segmenter.h"
+
+namespace icing {
+namespace lib {
+
+// Returns a vector containing all terms retrieved by Advancing on the iterator.
+std::vector<std::string_view> GetAllTermsAdvance(
+    LanguageSegmenter::Iterator* itr);
+
+// Returns a vector containing all terms retrieved by calling ResetAfter with
+// the UTF-32 position of the current term start to simulate Advancing on the
+// iterator.
+std::vector<std::string_view> GetAllTermsResetAfterUtf32(
+    LanguageSegmenter::Iterator* itr);
+
+// Returns a vector containing all terms retrieved by alternating calls to
+// Advance and calls to ResetAfter with the UTF-32 position of the current term
+// start to simulate Advancing.
+std::vector<std::string_view> GetAllTermsAdvanceAndResetAfterUtf32(
+    LanguageSegmenter::Iterator* itr);
+
+// Returns a vector containing all terms retrieved by calling ResetBefore with
+// the UTF-32 position of the current term start, starting at the end of the
+// text. This vector should be in reverse order of GetAllTerms and missing the
+// last term.
+std::vector<std::string_view> GetAllTermsResetBeforeUtf32(
+    LanguageSegmenter::Iterator* itr);
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_TESTING_ITERATOR_TEST_UTILS_H_
diff --git a/icing/testing/jni-test-helpers.h b/icing/testing/jni-test-helpers.h
index 67a98c3..401b401 100644
--- a/icing/testing/jni-test-helpers.h
+++ b/icing/testing/jni-test-helpers.h
@@ -24,19 +24,26 @@
 #include <jni.h>
 
 extern JNIEnv* g_jenv;
+extern char* g_icu_data_file_path;
 
 #define ICING_TEST_JNI_CACHE JniCache::Create(g_jenv).ValueOrDie()
+#define ICING_TEST_ICU_DATA_FILE_PATH g_icu_data_file_path
 
 #else
 
 #define ICING_TEST_JNI_CACHE nullptr
+#define ICING_TEST_ICU_DATA_FILE_PATH nullptr
 
 #endif  // ICING_REVERSE_JNI_SEGMENTATION
 
 namespace icing {
 namespace lib {
 
-std::unique_ptr<JniCache> GetTestJniCache() { return ICING_TEST_JNI_CACHE; }
+inline std::unique_ptr<JniCache> GetTestJniCache() {
+  return ICING_TEST_JNI_CACHE;
+}
+
+inline char* GetTestIcuDataFilePath() { return ICING_TEST_ICU_DATA_FILE_PATH; }
 
 }  // namespace lib
 }  // namespace icing
diff --git a/icing/testing/test-feature-flags.cc b/icing/testing/test-feature-flags.cc
new file mode 100644
index 0000000..c32e95f
--- /dev/null
+++ b/icing/testing/test-feature-flags.cc
@@ -0,0 +1,29 @@
+// Copyright (C) 2024 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/testing/test-feature-flags.h"
+
+#include "icing/feature-flags.h"
+
+namespace icing {
+namespace lib {
+
+FeatureFlags GetTestFeatureFlags() {
+  return FeatureFlags(/*enable_scorable_properties=*/true,
+                      /*enable_embedding_quantization=*/true,
+                      /*enable_repeated_field_joins=*/true);
+}
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/testing/test-feature-flags.h b/icing/testing/test-feature-flags.h
new file mode 100644
index 0000000..c84ec1a
--- /dev/null
+++ b/icing/testing/test-feature-flags.h
@@ -0,0 +1,28 @@
+// Copyright (C) 2024 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_TESTING_TEST_FEATURE_FLAGS_H_
+#define ICING_TESTING_TEST_FEATURE_FLAGS_H_
+
+#include "icing/feature-flags.h"
+
+namespace icing {
+namespace lib {
+
+FeatureFlags GetTestFeatureFlags();
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_TESTING_TEST_FEATURE_FLAGS_H_
diff --git a/icing/text_classifier/lib3/utils/base/statusor.h b/icing/text_classifier/lib3/utils/base/statusor.h
index aa1e598..4d9bb71 100644
--- a/icing/text_classifier/lib3/utils/base/statusor.h
+++ b/icing/text_classifier/lib3/utils/base/statusor.h
@@ -15,6 +15,7 @@
 #ifndef ICING_TEXT_CLASSIFIER_LIB3_UTILS_BASE_STATUSOR_H_
 #define ICING_TEXT_CLASSIFIER_LIB3_UTILS_BASE_STATUSOR_H_
 
+#include <cstdlib>
 #include <type_traits>
 #include <utility>
 
diff --git a/icing/tokenization/combined-tokenizer_test.cc b/icing/tokenization/combined-tokenizer_test.cc
index 0411748..04d56fe 100644
--- a/icing/tokenization/combined-tokenizer_test.cc
+++ b/icing/tokenization/combined-tokenizer_test.cc
@@ -24,6 +24,7 @@
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
+#include "icing/feature-flags.h"
 #include "icing/file/filesystem.h"
 #include "icing/file/portable-file-backed-proto-log.h"
 #include "icing/index/embed/embedding-index.h"
@@ -41,9 +42,9 @@
 #include "icing/store/document-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/jni-test-helpers.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"
@@ -52,6 +53,7 @@
 #include "icing/tokenization/tokenizer.h"
 #include "icing/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "icing/util/status-macros.h"
 #include "unicode/uloc.h"
 
@@ -76,6 +78,7 @@
         embedding_index_dir_(test_dir_ + "/embedding_index") {}
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     filesystem_.DeleteDirectoryRecursively(test_dir_.c_str());
     filesystem_.CreateDirectoryRecursively(index_dir_.c_str());
     filesystem_.CreateDirectoryRecursively(store_dir_.c_str());
@@ -83,25 +86,25 @@
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
     jni_cache_ = GetTestJniCache();
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
 
     ICING_ASSERT_OK_AND_ASSIGN(
         DocumentStore::CreateResult create_result,
-        DocumentStore::Create(
-            &filesystem_, store_dir_, &fake_clock_, schema_store_.get(),
-            /*force_recovery_and_revalidate_documents=*/false,
-            /*namespace_id_fingerprint=*/false, /*pre_mapping_fbv=*/false,
-            /*use_persistent_hash_map=*/false,
-            PortableFileBackedProtoLog<
-                DocumentWrapper>::kDeflateCompressionLevel,
-            /*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,
+                              /*initialize_stats=*/nullptr));
     document_store_ = std::move(create_result.document_store);
 
     Index::Options options(index_dir_,
@@ -116,7 +119,8 @@
         DummyNumericIndex<int64_t>::Create(filesystem_, numeric_index_dir_));
     ICING_ASSERT_OK_AND_ASSIGN(
         embedding_index_,
-        EmbeddingIndex::Create(&filesystem_, embedding_index_dir_));
+        EmbeddingIndex::Create(&filesystem_, embedding_index_dir_, &fake_clock_,
+                               feature_flags_.get()));
 
     language_segmenter_factory::SegmenterOptions segmenter_options(
         ULOC_US, jni_cache_.get());
@@ -128,10 +132,11 @@
                                                 /*max_term_byte_size=*/1000));
     ICING_ASSERT_OK_AND_ASSIGN(
         query_processor_,
-        QueryProcessor::Create(index_.get(), numeric_index_.get(),
-                               embedding_index_.get(), lang_segmenter_.get(),
-                               normalizer_.get(), document_store_.get(),
-                               schema_store_.get(), &fake_clock_));
+        QueryProcessor::Create(
+            index_.get(), numeric_index_.get(), embedding_index_.get(),
+            lang_segmenter_.get(), normalizer_.get(), document_store_.get(),
+            schema_store_.get(), /*join_children_fetcher=*/nullptr,
+            &fake_clock_, feature_flags_.get()));
   }
 
   libtextclassifier3::StatusOr<std::vector<std::string>> GetQueryTerms(
@@ -154,6 +159,7 @@
     return query_terms;
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   const std::string test_dir_;
   const std::string store_dir_;
diff --git a/icing/tokenization/icu/icu-language-segmenter-factory.cc b/icing/tokenization/icu/icu-language-segmenter-factory.cc
index 7b095b4..4f8263b 100644
--- a/icing/tokenization/icu/icu-language-segmenter-factory.cc
+++ b/icing/tokenization/icu/icu-language-segmenter-factory.cc
@@ -30,7 +30,7 @@
 //
 // Returns:
 //   A LanguageSegmenter on success
-//   INVALID_ARGUMENT if locale string is invalid
+//   INVALID_ARGUMENT_ERROR if locale string is invalid
 //
 // TODO(b/156383798): Figure out if we want to verify locale strings and notify
 // users. Right now illegal locale strings will be ignored by ICU. ICU
diff --git a/icing/tokenization/icu/icu-language-segmenter_test.cc b/icing/tokenization/icu/icu-language-segmenter_test.cc
index a7f7419..664ccce 100644
--- a/icing/tokenization/icu/icu-language-segmenter_test.cc
+++ b/icing/tokenization/icu/icu-language-segmenter_test.cc
@@ -23,13 +23,13 @@
 #include "icing/jni/jni-cache.h"
 #include "icing/portable/platform.h"
 #include "icing/testing/common-matchers.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/icu-i18n-test-utils.h"
+#include "icing/testing/iterator-test-utils.h"
 #include "icing/testing/jni-test-helpers.h"
 #include "icing/testing/test-data.h"
 #include "icing/tokenization/language-segmenter-factory.h"
 #include "icing/tokenization/language-segmenter.h"
-#include "icing/util/character-iterator.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
 namespace icing {
@@ -41,80 +41,6 @@
 
 namespace {
 
-language_segmenter_factory::SegmenterOptions GetSegmenterOptions(
-    const std::string& locale, const JniCache* jni_cache) {
-  return language_segmenter_factory::SegmenterOptions(locale, jni_cache);
-}
-
-// Returns a vector containing all terms retrieved by Advancing on the iterator.
-std::vector<std::string_view> GetAllTermsAdvance(
-    LanguageSegmenter::Iterator* itr) {
-  std::vector<std::string_view> terms;
-  while (itr->Advance()) {
-    terms.push_back(itr->GetTerm());
-  }
-  return terms;
-}
-
-// Returns a vector containing all terms retrieved by calling ResetAfter with
-// the UTF-32 position of the current term start to simulate Advancing on the
-// iterator.
-std::vector<std::string_view> GetAllTermsResetAfterUtf32(
-    LanguageSegmenter::Iterator* itr) {
-  std::vector<std::string_view> terms;
-  // Calling ResetToTermStartingAfterUtf32 with -1 should get the first term in
-  // the sequence.
-  bool is_ok = itr->ResetToTermStartingAfterUtf32(-1).ok();
-  while (is_ok) {
-    terms.push_back(itr->GetTerm());
-    // Calling ResetToTermStartingAfterUtf32 with the current position should
-    // get the very next term in the sequence.
-    CharacterIterator char_itr = itr->CalculateTermStart().ValueOrDie();
-    is_ok = itr->ResetToTermStartingAfterUtf32(char_itr.utf32_index()).ok();
-  }
-  return terms;
-}
-
-// Returns a vector containing all terms retrieved by alternating calls to
-// Advance and calls to ResetAfter with the UTF-32 position of the current term
-// start to simulate Advancing.
-std::vector<std::string_view> GetAllTermsAdvanceAndResetAfterUtf32(
-    LanguageSegmenter::Iterator* itr) {
-  std::vector<std::string_view> terms;
-  bool is_ok = itr->Advance();
-  while (is_ok) {
-    terms.push_back(itr->GetTerm());
-    // Alternate between using Advance and ResetToTermAfter.
-    if (terms.size() % 2 == 0) {
-      is_ok = itr->Advance();
-    } else {
-      // Calling ResetToTermStartingAfterUtf32 with the current position should
-      // get the very next term in the sequence.
-      CharacterIterator char_itr = itr->CalculateTermStart().ValueOrDie();
-      is_ok = itr->ResetToTermStartingAfterUtf32(char_itr.utf32_index()).ok();
-    }
-  }
-  return terms;
-}
-
-// Returns a vector containing all terms retrieved by calling ResetBefore with
-// the UTF-32 position of the current term start, starting at the end of the
-// text. This vector should be in reverse order of GetAllTerms and missing the
-// last term.
-std::vector<std::string_view> GetAllTermsResetBeforeUtf32(
-    LanguageSegmenter::Iterator* itr) {
-  std::vector<std::string_view> terms;
-  bool is_ok = itr->ResetToTermEndingBeforeUtf32(1000).ok();
-  while (is_ok) {
-    terms.push_back(itr->GetTerm());
-    // Calling ResetToTermEndingBeforeUtf32 with the current position should get
-    // the previous term in the sequence.
-    CharacterIterator char_itr = itr->CalculateTermStart().ValueOrDie();
-    is_ok = itr->ResetToTermEndingBeforeUtf32(char_itr.utf32_index()).ok();
-  }
-  return terms;
-}
-
 class IcuLanguageSegmenterAllLocalesTest
     : public testing::TestWithParam<const char*> {
  protected:
@@ -124,7 +50,7 @@
     }
     ICING_ASSERT_OK(
         // File generated via icu_data_file rule in //icing/BUILD.
-        icu_data_file_helper::SetUpICUDataFile(
+        icu_data_file_helper::SetUpIcuDataFile(
             GetTestFilePath("icing/icu.dat")));
   }
 
@@ -139,7 +65,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   EXPECT_THAT(language_segmenter->GetAllTerms(""), IsOkAndHolds(IsEmpty()));
 }
 
@@ -147,7 +74,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   EXPECT_THAT(language_segmenter->GetAllTerms("Hello World"),
               IsOkAndHolds(ElementsAre("Hello", " ", "World")));
 }
@@ -156,7 +84,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // ASCII punctuation marks are kept
   EXPECT_THAT(
       language_segmenter->GetAllTerms("Hello, World!!!"),
@@ -173,7 +102,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // ASCII special characters are kept
   EXPECT_THAT(language_segmenter->GetAllTerms("Pay $1000"),
               IsOkAndHolds(ElementsAre("Pay", " ", "$", "1000")));
@@ -191,7 +121,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Full-width (non-ASCII) punctuation marks and special characters are left
   // out.
   EXPECT_THAT(language_segmenter->GetAllTerms("。?·Hello×"),
@@ -202,7 +133,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   EXPECT_THAT(language_segmenter->GetAllTerms("U.S.𡔖 Bank"),
               IsOkAndHolds(ElementsAre("U.S", ".", "𡔖", " ", "Bank")));
   EXPECT_THAT(language_segmenter->GetAllTerms("I.B.M."),
@@ -217,7 +149,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // According to unicode word break rules
   // WB6(https://unicode.org/reports/tr29/#WB6),
   // WB7(https://unicode.org/reports/tr29/#WB7), and a few others, some
@@ -326,7 +259,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   EXPECT_THAT(language_segmenter->GetAllTerms("It's ok."),
               IsOkAndHolds(ElementsAre("It's", " ", "ok", ".")));
   EXPECT_THAT(language_segmenter->GetAllTerms("He'll be back."),
@@ -349,7 +283,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
 
   EXPECT_THAT(language_segmenter->GetAllTerms("(Hello)"),
               IsOkAndHolds(ElementsAre("(", "Hello", ")")));
@@ -362,7 +297,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
 
   EXPECT_THAT(language_segmenter->GetAllTerms("\"Hello\""),
               IsOkAndHolds(ElementsAre("\"", "Hello", "\"")));
@@ -375,7 +311,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
 
   // Alphanumeric terms are allowed
   EXPECT_THAT(language_segmenter->GetAllTerms("Se7en A4 3a"),
@@ -386,7 +323,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
 
   // Alphanumeric terms are allowed
   EXPECT_THAT(
@@ -404,7 +342,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   EXPECT_THAT(language_segmenter->GetAllTerms("0123456789"),
               IsOkAndHolds(ElementsAre("0123456789")));
 }
@@ -413,7 +352,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Multiple continuous whitespaces are treated as one.
   const int kNumSeparators = 256;
   std::string text_with_spaces =
@@ -440,7 +380,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // CJKT (Chinese, Japanese, Khmer, Thai) are the 4 main languages that don't
   // have whitespaces as word delimiter.
 
@@ -465,7 +406,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   EXPECT_THAT(language_segmenter->GetAllTerms("āăąḃḅḇčćç"),
               IsOkAndHolds(ElementsAre("āăąḃḅḇčćç")));
 }
@@ -474,7 +416,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Turkish
   EXPECT_THAT(language_segmenter->GetAllTerms("merhaba dünya"),
               IsOkAndHolds(ElementsAre("merhaba", " ", "dünya")));
@@ -488,7 +431,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   EXPECT_THAT(language_segmenter->GetAllTerms("How are you你好吗お元気ですか"),
               IsOkAndHolds(ElementsAre("How", " ", "are", " ", "you", "你弽",
                                        "吗", "お", "元気", "です", "か")));
@@ -502,7 +446,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Validates that the input strings are not copied
   const std::string text = "Hello World";
   const char* word1_address = text.c_str();
@@ -520,11 +465,13 @@
 
 TEST_P(IcuLanguageSegmenterAllLocalesTest, ResetToStartUtf32WordConnector) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "com.google.android is package";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:      "com.google.android is package"
   //               ^                 ^^ ^^
@@ -537,11 +484,13 @@
 
 TEST_P(IcuLanguageSegmenterAllLocalesTest, NewIteratorResetToStartUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:     "How are you你好吗お元気ですか"
   //              ^  ^^  ^^  ^  ^ ^ ^  ^  ^
@@ -554,11 +503,13 @@
 TEST_P(IcuLanguageSegmenterAllLocalesTest,
        IteratorOneAdvanceResetToStartUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:     "How are you你好吗お元気ですか"
   //              ^  ^^  ^^  ^  ^ ^ ^  ^  ^
@@ -572,11 +523,13 @@
 TEST_P(IcuLanguageSegmenterAllLocalesTest,
        IteratorMultipleAdvancesResetToStartUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:     "How are you你好吗お元気ですか"
   //              ^  ^^  ^^  ^  ^ ^ ^  ^  ^
@@ -592,11 +545,13 @@
 
 TEST_P(IcuLanguageSegmenterAllLocalesTest, IteratorDoneResetToStartUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:     "How are you你好吗お元気ですか"
   //              ^  ^^  ^^  ^  ^ ^ ^  ^  ^
@@ -611,11 +566,13 @@
 
 TEST_P(IcuLanguageSegmenterAllLocalesTest, ResetToTermAfterUtf32WordConnector) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "package com.google.android name";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:     "package com.google.android name"
   //              ^      ^^                 ^^
@@ -634,11 +591,13 @@
 
 TEST_P(IcuLanguageSegmenterAllLocalesTest, ResetToTermAfterUtf32OutOfBounds) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:     "How are you你好吗お元気ですか"
   //              ^  ^^  ^^  ^  ^ ^ ^  ^  ^
@@ -656,25 +615,27 @@
 }
 
 // Tests that ResetToTermAfter and Advance produce the same output. With the
-// exception of the first term which is inacessible via ResetToTermAfter,
-// the stream of terms produced by Advance calls should exacly match the
+// exception of the first term which is inaccessible via ResetToTermAfter,
+// the stream of terms produced by Advance calls should exactly match the
 // terms produced by ResetToTermAfter calls with the current position
 // provided as the argument.
 TEST_P(IcuLanguageSegmenterAllLocalesTest,
        MixedLanguagesResetToTermAfterUtf32EquivalentToAdvance) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are𡔖 you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kText));
+      language_segmenter->Segment(kText));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> reset_to_term_itr,
-      segmenter->Segment(kText));
+      language_segmenter->Segment(kText));
   std::vector<std::string_view> reset_terms =
       GetAllTermsResetAfterUtf32(reset_to_term_itr.get());
 
@@ -685,18 +646,20 @@
 TEST_P(IcuLanguageSegmenterAllLocalesTest,
        ThaiResetToTermAfterUtf32EquivalentToAdvance) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kThai = "ฉันเดินไปทำงานทุกวัน";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kThai));
+      language_segmenter->Segment(kThai));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> reset_to_term_itr,
-      segmenter->Segment(kThai));
+      language_segmenter->Segment(kThai));
   std::vector<std::string_view> reset_terms =
       GetAllTermsResetAfterUtf32(reset_to_term_itr.get());
 
@@ -707,18 +670,20 @@
 TEST_P(IcuLanguageSegmenterAllLocalesTest,
        KoreanResetToTermAfterUtf32EquivalentToAdvance) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kKorean = "나는 매일 출근합니다.";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kKorean));
+      language_segmenter->Segment(kKorean));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> reset_to_term_itr,
-      segmenter->Segment(kKorean));
+      language_segmenter->Segment(kKorean));
   std::vector<std::string_view> reset_terms =
       GetAllTermsResetAfterUtf32(reset_to_term_itr.get());
 
@@ -733,18 +698,20 @@
 TEST_P(IcuLanguageSegmenterAllLocalesTest,
        MixedLanguagesResetToTermAfterUtf32InteroperableWithAdvance) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are𡔖 you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kText));
+      language_segmenter->Segment(kText));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_and_reset_itr,
-      segmenter->Segment(kText));
+      language_segmenter->Segment(kText));
   std::vector<std::string_view> advance_and_reset_terms =
       GetAllTermsAdvanceAndResetAfterUtf32(advance_and_reset_itr.get());
 
@@ -756,18 +723,20 @@
 TEST_P(IcuLanguageSegmenterAllLocalesTest,
        ThaiResetToTermAfterUtf32InteroperableWithAdvance) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kThai = "ฉันเดินไปทำงานทุกวัน";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kThai));
+      language_segmenter->Segment(kThai));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_and_reset_itr,
-      segmenter->Segment(kThai));
+      language_segmenter->Segment(kThai));
   std::vector<std::string_view> advance_and_reset_terms =
       GetAllTermsAdvanceAndResetAfterUtf32(advance_and_reset_itr.get());
 
@@ -779,18 +748,20 @@
 TEST_P(IcuLanguageSegmenterAllLocalesTest,
        KoreanResetToTermAfterUtf32InteroperableWithAdvance) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kKorean = "나는 매일 출근합니다.";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kKorean));
+      language_segmenter->Segment(kKorean));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_and_reset_itr,
-      segmenter->Segment(kKorean));
+      language_segmenter->Segment(kKorean));
   std::vector<std::string_view> advance_and_reset_terms =
       GetAllTermsAdvanceAndResetAfterUtf32(advance_and_reset_itr.get());
 
@@ -804,7 +775,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> itr,
       language_segmenter->Segment("How are you你好吗お元気ですか"));
@@ -841,7 +813,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          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,
@@ -880,7 +853,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // CJKT (Chinese, Japanese, Khmer, Thai) are the 4 main languages that
   // don't have whitespaces as word delimiter. Chinese
   constexpr std::string_view kChinese = "我每天走路去上班。";
@@ -908,7 +882,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Japanese
   constexpr std::string_view kJapanese = "私は毎日仕事に歩いています。";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
@@ -935,7 +910,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kKhmer = "ញុំដើរទៅធ្វើការរាល់ថ្ងៃ។";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
                              language_segmenter->Segment(kKhmer));
@@ -961,7 +937,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Thai
   constexpr std::string_view kThai = "ฉันเดินไปทำงานทุกวัน";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
@@ -987,11 +964,13 @@
 TEST_P(IcuLanguageSegmenterAllLocalesTest,
        ResetToTermBeforeWordConnectorUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "package name com.google.android!";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:      "package name com.google.android!"
   //               ^      ^^   ^^                 ^
@@ -1010,11 +989,13 @@
 
 TEST_P(IcuLanguageSegmenterAllLocalesTest, ResetToTermBeforeOutOfBoundsUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:      "How are you你好吗お元気ですか"
   //               ^  ^^  ^^  ^  ^ ^ ^  ^  ^
@@ -1032,25 +1013,27 @@
 }
 
 // Tests that ResetToTermBefore and Advance produce the same output. With the
-// exception of the last term which is inacessible via ResetToTermBefore,
-// the stream of terms produced by Advance calls should exacly match the
+// exception of the last term which is inaccessible via ResetToTermBefore,
+// the stream of terms produced by Advance calls should exactly match the
 // terms produced by ResetToTermBefore calls with the current position
 // provided as the argument (after their order has been reversed).
 TEST_P(IcuLanguageSegmenterAllLocalesTest,
        MixedLanguagesResetToTermBeforeEquivalentToAdvanceUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are𡔖 you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kText));
+      language_segmenter->Segment(kText));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> reset_to_term_itr,
-      segmenter->Segment(kText));
+      language_segmenter->Segment(kText));
   std::vector<std::string_view> reset_terms =
       GetAllTermsResetBeforeUtf32(reset_to_term_itr.get());
   std::reverse(reset_terms.begin(), reset_terms.end());
@@ -1063,18 +1046,20 @@
 TEST_P(IcuLanguageSegmenterAllLocalesTest,
        ThaiResetToTermBeforeEquivalentToAdvanceUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kThai = "ฉันเดินไปทำงานทุกวัน";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kThai));
+      language_segmenter->Segment(kThai));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> reset_to_term_itr,
-      segmenter->Segment(kThai));
+      language_segmenter->Segment(kThai));
   std::vector<std::string_view> reset_terms =
       GetAllTermsResetBeforeUtf32(reset_to_term_itr.get());
   std::reverse(reset_terms.begin(), reset_terms.end());
@@ -1086,18 +1071,20 @@
 TEST_P(IcuLanguageSegmenterAllLocalesTest,
        KoreanResetToTermBeforeEquivalentToAdvanceUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kKorean = "나는 매일 출근합니다.";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kKorean));
+      language_segmenter->Segment(kKorean));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> reset_to_term_itr,
-      segmenter->Segment(kKorean));
+      language_segmenter->Segment(kKorean));
   std::vector<std::string_view> reset_terms =
       GetAllTermsResetBeforeUtf32(reset_to_term_itr.get());
   std::reverse(reset_terms.begin(), reset_terms.end());
@@ -1111,7 +1098,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> itr,
       language_segmenter->Segment("How are you你好吗お元気ですか"));
@@ -1149,7 +1137,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          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,
@@ -1187,7 +1176,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // CJKT (Chinese, Japanese, Khmer, Thai) are the 4 main languages that
   // don't have whitespaces as word delimiter. Chinese
   constexpr std::string_view kChinese = "我每天走路去上班。";
@@ -1212,7 +1202,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Japanese
   constexpr std::string_view kJapanese = "私は毎日仕事に歩いています。";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
@@ -1236,7 +1227,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kKhmer = "ញុំដើរទៅធ្វើការរាល់ថ្ងៃ។";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
                              language_segmenter->Segment(kKhmer));
@@ -1259,7 +1251,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Thai
   constexpr std::string_view kThai = "ฉันเดินไปทำงานทุกวัน";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
@@ -1286,7 +1279,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Validates that the input strings are not copied
   ICING_ASSERT_OK_AND_ASSIGN(
       std::vector<std::string_view> terms,
@@ -1301,7 +1295,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> iterator_one,
diff --git a/icing/tokenization/language-segmenter-factory.h b/icing/tokenization/language-segmenter-factory.h
index 2505a07..ff7b781 100644
--- a/icing/tokenization/language-segmenter-factory.h
+++ b/icing/tokenization/language-segmenter-factory.h
@@ -17,6 +17,7 @@
 
 #include <memory>
 #include <string_view>
+#include <utility>
 
 #include "icing/text_classifier/lib3/utils/base/statusor.h"
 #include "icing/jni/jni-cache.h"
@@ -29,20 +30,32 @@
 
 struct SegmenterOptions {
   explicit SegmenterOptions(std::string locale,
-                            const JniCache* jni_cache = nullptr)
-      : locale(std::move(locale)), jni_cache(jni_cache) {}
+                            const JniCache* jni_cache = nullptr,
+                            bool enable_icu_segmenter = false)
+      : locale(std::move(locale)),
+        jni_cache(jni_cache),
+        enable_icu_segmenter(enable_icu_segmenter) {}
 
   std::string locale;
 
   // Does not hold ownership.
   const JniCache* jni_cache;
+
+  // Determines whether to use an ICU based language segmenter
+  // in icu-with-reverse-jni-language-segmenter-factory or not.
+  // The default value is false, which means that the fallback option of a
+  // Reverse JNI based language segmenter will be used.
+  //
+  // This variable is a no-op for all other segmenter factories because they
+  // only support one segmenter type.
+  bool enable_icu_segmenter;
 };
 
 // Creates a language segmenter with the given locale.
 //
 // Returns:
 //   A LanguageSegmenter on success
-//   INVALID_ARGUMENT if locale string is invalid
+//   INVALID_ARGUMENT_ERROR if locale string is invalid
 libtextclassifier3::StatusOr<std::unique_ptr<LanguageSegmenter>> Create(
     SegmenterOptions options);
 
diff --git a/icing/tokenization/language-segmenter-iterator_test.cc b/icing/tokenization/language-segmenter-iterator_test.cc
index 3aff45c..d9d71c6 100644
--- a/icing/tokenization/language-segmenter-iterator_test.cc
+++ b/icing/tokenization/language-segmenter-iterator_test.cc
@@ -17,11 +17,11 @@
 #include "icing/absl_ports/str_cat.h"
 #include "icing/portable/platform.h"
 #include "icing/testing/common-matchers.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/jni-test-helpers.h"
 #include "icing/testing/test-data.h"
 #include "icing/tokenization/language-segmenter-factory.h"
 #include "icing/tokenization/language-segmenter.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
 namespace icing {
@@ -40,7 +40,7 @@
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
   }
diff --git a/icing/tokenization/language-segmenter_benchmark.cc b/icing/tokenization/language-segmenter_benchmark.cc
index 748a322..1ddcfb8 100644
--- a/icing/tokenization/language-segmenter_benchmark.cc
+++ b/icing/tokenization/language-segmenter_benchmark.cc
@@ -15,11 +15,11 @@
 #include "testing/base/public/benchmark.h"
 #include "gmock/gmock.h"
 #include "icing/testing/common-matchers.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/test-data.h"
 #include "icing/tokenization/language-segmenter-factory.h"
 #include "icing/tokenization/language-segmenter.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
 // Run on a Linux workstation:
@@ -56,7 +56,7 @@
 void BM_SegmentNoSpace(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
@@ -93,7 +93,7 @@
 void BM_SegmentWithSpaces(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
@@ -133,7 +133,7 @@
 void BM_SegmentCJK(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
diff --git a/icing/tokenization/plain-tokenizer_test.cc b/icing/tokenization/plain-tokenizer_test.cc
index 6c426da..a0d187d 100644
--- a/icing/tokenization/plain-tokenizer_test.cc
+++ b/icing/tokenization/plain-tokenizer_test.cc
@@ -20,12 +20,12 @@
 #include "icing/absl_ports/str_cat.h"
 #include "icing/portable/platform.h"
 #include "icing/testing/common-matchers.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/icu-i18n-test-utils.h"
 #include "icing/testing/jni-test-helpers.h"
 #include "icing/testing/test-data.h"
 #include "icing/tokenization/language-segmenter-factory.h"
 #include "icing/tokenization/tokenizer-factory.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
 namespace icing {
@@ -40,7 +40,7 @@
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
   }
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 a251f90..1581e13 100644
--- a/icing/tokenization/reverse_jni/reverse-jni-language-segmenter-factory.cc
+++ b/icing/tokenization/reverse_jni/reverse-jni-language-segmenter-factory.cc
@@ -32,7 +32,7 @@
 //
 // Returns:
 //   A LanguageSegmenter on success
-//   INVALID_ARGUMENT if locale string is invalid
+//   INVALID_ARGUMENT_ERROR if locale string is invalid
 //
 // TODO(b/156383798): Figure out if we want to verify locale strings and notify
 // users. Right now illegal locale strings will be ignored by ICU. ICU
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 47a01fe..683c5f9 100644
--- a/icing/tokenization/reverse_jni/reverse-jni-language-segmenter_test.cc
+++ b/icing/tokenization/reverse_jni/reverse-jni-language-segmenter_test.cc
@@ -24,10 +24,10 @@
 #include "icing/jni/jni-cache.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/icu-i18n-test-utils.h"
+#include "icing/testing/iterator-test-utils.h"
 #include "icing/testing/jni-test-helpers.h"
 #include "icing/tokenization/language-segmenter-factory.h"
 #include "icing/tokenization/language-segmenter.h"
-#include "icing/util/character-iterator.h"
 #include "unicode/uloc.h"
 
 namespace icing {
@@ -41,80 +41,6 @@
 
 namespace {
 
-language_segmenter_factory::SegmenterOptions GetSegmenterOptions(
-    const std::string& locale, const JniCache* jni_cache) {
-  return language_segmenter_factory::SegmenterOptions(locale, jni_cache);
-}
-
-// Returns a vector containing all terms retrieved by Advancing on the iterator.
-std::vector<std::string_view> GetAllTermsAdvance(
-    LanguageSegmenter::Iterator* itr) {
-  std::vector<std::string_view> terms;
-  while (itr->Advance()) {
-    terms.push_back(itr->GetTerm());
-  }
-  return terms;
-}
-
-// Returns a vector containing all terms retrieved by calling ResetAfter with
-// the UTF-32 position of the current term start to simulate Advancing on the
-// iterator.
-std::vector<std::string_view> GetAllTermsResetAfterUtf32(
-    LanguageSegmenter::Iterator* itr) {
-  std::vector<std::string_view> terms;
-  // Calling ResetToTermStartingAfterUtf32 with -1 should get the first term in
-  // the sequence.
-  bool is_ok = itr->ResetToTermStartingAfterUtf32(-1).ok();
-  while (is_ok) {
-    terms.push_back(itr->GetTerm());
-    // Calling ResetToTermStartingAfterUtf32 with the current position should
-    // get the very next term in the sequence.
-    CharacterIterator char_itr = itr->CalculateTermStart().ValueOrDie();
-    is_ok = itr->ResetToTermStartingAfterUtf32(char_itr.utf32_index()).ok();
-  }
-  return terms;
-}
-
-// Returns a vector containing all terms retrieved by alternating calls to
-// Advance and calls to ResetAfter with the UTF-32 position of the current term
-// start to simulate Advancing.
-std::vector<std::string_view> GetAllTermsAdvanceAndResetAfterUtf32(
-    LanguageSegmenter::Iterator* itr) {
-  std::vector<std::string_view> terms;
-  bool is_ok = itr->Advance();
-  while (is_ok) {
-    terms.push_back(itr->GetTerm());
-    // Alternate between using Advance and ResetToTermAfter.
-    if (terms.size() % 2 == 0) {
-      is_ok = itr->Advance();
-    } else {
-      // Calling ResetToTermStartingAfterUtf32 with the current position should
-      // get the very next term in the sequence.
-      CharacterIterator char_itr = itr->CalculateTermStart().ValueOrDie();
-      is_ok = itr->ResetToTermStartingAfterUtf32(char_itr.utf32_index()).ok();
-    }
-  }
-  return terms;
-}
-
-// Returns a vector containing all terms retrieved by calling ResetBefore with
-// the UTF-32 position of the current term start, starting at the end of the
-// text. This vector should be in reverse order of GetAllTerms and missing the
-// last term.
-std::vector<std::string_view> GetAllTermsResetBeforeUtf32(
-    LanguageSegmenter::Iterator* itr) {
-  std::vector<std::string_view> terms;
-  bool is_ok = itr->ResetToTermEndingBeforeUtf32(1000).ok();
-  while (is_ok) {
-    terms.push_back(itr->GetTerm());
-    // Calling ResetToTermEndingBeforeUtf32 with the current position should get
-    // the previous term in the sequence.
-    CharacterIterator char_itr = itr->CalculateTermStart().ValueOrDie();
-    is_ok = itr->ResetToTermEndingBeforeUtf32(char_itr.utf32_index()).ok();
-  }
-  return terms;
-}
-
 class ReverseJniLanguageSegmenterTest
     : public testing::TestWithParam<const char*> {
  protected:
@@ -129,7 +55,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   EXPECT_THAT(language_segmenter->GetAllTerms(""), IsOkAndHolds(IsEmpty()));
 }
 
@@ -137,7 +64,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   EXPECT_THAT(language_segmenter->GetAllTerms("Hello World"),
               IsOkAndHolds(ElementsAre("Hello", " ", "World")));
 }
@@ -146,7 +74,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // ASCII punctuation marks are kept
   EXPECT_THAT(
       language_segmenter->GetAllTerms("Hello, World!!!"),
@@ -163,7 +92,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // ASCII special characters are kept
   EXPECT_THAT(language_segmenter->GetAllTerms("Pay $1000"),
               IsOkAndHolds(ElementsAre("Pay", " ", "$", "1000")));
@@ -181,7 +111,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Full-width (non-ASCII) punctuation marks and special characters are left
   // out.
   EXPECT_THAT(language_segmenter->GetAllTerms("。?·Hello×"),
@@ -192,7 +123,9 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get(),
+              /*use_icu_segmenter=*/true)));
   EXPECT_THAT(language_segmenter->GetAllTerms("U.S.𡔖 Bank"),
               IsOkAndHolds(ElementsAre("U.S", ".", "𡔖", " ", "Bank")));
   EXPECT_THAT(language_segmenter->GetAllTerms("I.B.M."),
@@ -207,7 +140,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // According to unicode word break rules
   // WB6(https://unicode.org/reports/tr29/#WB6),
   // WB7(https://unicode.org/reports/tr29/#WB7), and a few others, some
@@ -292,7 +226,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   EXPECT_THAT(language_segmenter->GetAllTerms("It's ok."),
               IsOkAndHolds(ElementsAre("It's", " ", "ok", ".")));
   EXPECT_THAT(language_segmenter->GetAllTerms("He'll be back."),
@@ -315,7 +250,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
 
   EXPECT_THAT(language_segmenter->GetAllTerms("(Hello)"),
               IsOkAndHolds(ElementsAre("(", "Hello", ")")));
@@ -328,7 +264,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
 
   EXPECT_THAT(language_segmenter->GetAllTerms("\"Hello\""),
               IsOkAndHolds(ElementsAre("\"", "Hello", "\"")));
@@ -341,7 +278,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
 
   // Alphanumeric terms are allowed
   EXPECT_THAT(language_segmenter->GetAllTerms("Se7en A4 3a"),
@@ -352,7 +290,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
 
   // Alphanumeric terms are allowed
   EXPECT_THAT(
@@ -370,7 +309,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
 
   EXPECT_THAT(language_segmenter->GetAllTerms("0123456789"),
               IsOkAndHolds(ElementsAre("0", "1", "2", "3", "4", "5", "6",
@@ -381,7 +321,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Multiple continuous whitespaces are treated as one.
   const int kNumSeparators = 256;
   std::string text_with_spaces =
@@ -408,7 +349,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // CJKT (Chinese, Japanese, Khmer, Thai) are the 4 main languages that don't
   // have whitespaces as word delimiter.
 
@@ -433,7 +375,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   EXPECT_THAT(language_segmenter->GetAllTerms("āăąḃḅḇčćç"),
               IsOkAndHolds(ElementsAre("āăąḃḅḇčćç")));
 }
@@ -442,7 +385,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Turkish
   EXPECT_THAT(language_segmenter->GetAllTerms("merhaba dünya"),
               IsOkAndHolds(ElementsAre("merhaba", " ", "dünya")));
@@ -456,7 +400,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   EXPECT_THAT(language_segmenter->GetAllTerms("How are you你好吗お元気ですか"),
               IsOkAndHolds(ElementsAre("How", " ", "are", " ", "you", "你弽",
                                        "吗", "お", "元気", "です", "か")));
@@ -470,7 +415,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Validates that the input strings are not copied
   const std::string text = "Hello World";
   const char* word1_address = text.c_str();
@@ -488,11 +434,13 @@
 
 TEST_P(ReverseJniLanguageSegmenterTest, ResetToStartUtf32WordConnector) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "com:google:android is package";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:      "com:google:android is package"
   //               ^                 ^^ ^^
@@ -505,11 +453,13 @@
 
 TEST_P(ReverseJniLanguageSegmenterTest, NewIteratorResetToStartUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:     "How are you你好吗お元気ですか"
   //              ^  ^^  ^^  ^  ^ ^ ^  ^  ^
@@ -521,11 +471,13 @@
 
 TEST_P(ReverseJniLanguageSegmenterTest, IteratorOneAdvanceResetToStartUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:     "How are you你好吗お元気ですか"
   //              ^  ^^  ^^  ^  ^ ^ ^  ^  ^
@@ -539,11 +491,13 @@
 TEST_P(ReverseJniLanguageSegmenterTest,
        IteratorMultipleAdvancesResetToStartUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:     "How are you你好吗お元気ですか"
   //              ^  ^^  ^^  ^  ^ ^ ^  ^  ^
@@ -559,11 +513,13 @@
 
 TEST_P(ReverseJniLanguageSegmenterTest, IteratorDoneResetToStartUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:     "How are you你好吗お元気ですか"
   //              ^  ^^  ^^  ^  ^ ^ ^  ^  ^
@@ -578,11 +534,13 @@
 
 TEST_P(ReverseJniLanguageSegmenterTest, ResetToTermAfterUtf32WordConnector) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "package com:google:android name";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:     "package com:google:android name"
   //              ^      ^^                 ^^
@@ -601,11 +559,13 @@
 
 TEST_P(ReverseJniLanguageSegmenterTest, ResetToTermAfterUtf32OutOfBounds) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:     "How are you你好吗お元気ですか"
   //              ^  ^^  ^^  ^  ^ ^ ^  ^  ^
@@ -623,25 +583,27 @@
 }
 
 // Tests that ResetToTermAfter and Advance produce the same output. With the
-// exception of the first term which is inacessible via ResetToTermAfter,
-// the stream of terms produced by Advance calls should exacly match the
+// exception of the first term which is inaccessible via ResetToTermAfter,
+// the stream of terms produced by Advance calls should exactly match the
 // terms produced by ResetToTermAfter calls with the current position
 // provided as the argument.
 TEST_P(ReverseJniLanguageSegmenterTest,
        MixedLanguagesResetToTermAfterUtf32EquivalentToAdvance) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are𡔖 you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kText));
+      language_segmenter->Segment(kText));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> reset_to_term_itr,
-      segmenter->Segment(kText));
+      language_segmenter->Segment(kText));
   std::vector<std::string_view> reset_terms =
       GetAllTermsResetAfterUtf32(reset_to_term_itr.get());
 
@@ -652,18 +614,20 @@
 TEST_P(ReverseJniLanguageSegmenterTest,
        ThaiResetToTermAfterUtf32EquivalentToAdvance) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kThai = "ฉันเดินไปทำงานทุกวัน";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kThai));
+      language_segmenter->Segment(kThai));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> reset_to_term_itr,
-      segmenter->Segment(kThai));
+      language_segmenter->Segment(kThai));
   std::vector<std::string_view> reset_terms =
       GetAllTermsResetAfterUtf32(reset_to_term_itr.get());
 
@@ -674,18 +638,20 @@
 TEST_P(ReverseJniLanguageSegmenterTest,
        KoreanResetToTermAfterUtf32EquivalentToAdvance) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kKorean = "나는 매일 출근합니다.";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kKorean));
+      language_segmenter->Segment(kKorean));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> reset_to_term_itr,
-      segmenter->Segment(kKorean));
+      language_segmenter->Segment(kKorean));
   std::vector<std::string_view> reset_terms =
       GetAllTermsResetAfterUtf32(reset_to_term_itr.get());
 
@@ -700,18 +666,20 @@
 TEST_P(ReverseJniLanguageSegmenterTest,
        MixedLanguagesResetToTermAfterUtf32InteroperableWithAdvance) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are𡔖 you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kText));
+      language_segmenter->Segment(kText));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_and_reset_itr,
-      segmenter->Segment(kText));
+      language_segmenter->Segment(kText));
   std::vector<std::string_view> advance_and_reset_terms =
       GetAllTermsAdvanceAndResetAfterUtf32(advance_and_reset_itr.get());
 
@@ -723,18 +691,20 @@
 TEST_P(ReverseJniLanguageSegmenterTest,
        ThaiResetToTermAfterUtf32InteroperableWithAdvance) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kThai = "ฉันเดินไปทำงานทุกวัน";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kThai));
+      language_segmenter->Segment(kThai));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_and_reset_itr,
-      segmenter->Segment(kThai));
+      language_segmenter->Segment(kThai));
   std::vector<std::string_view> advance_and_reset_terms =
       GetAllTermsAdvanceAndResetAfterUtf32(advance_and_reset_itr.get());
 
@@ -746,18 +716,20 @@
 TEST_P(ReverseJniLanguageSegmenterTest,
        KoreanResetToTermAfterUtf32InteroperableWithAdvance) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kKorean = "나는 매일 출근합니다.";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kKorean));
+      language_segmenter->Segment(kKorean));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_and_reset_itr,
-      segmenter->Segment(kKorean));
+      language_segmenter->Segment(kKorean));
   std::vector<std::string_view> advance_and_reset_terms =
       GetAllTermsAdvanceAndResetAfterUtf32(advance_and_reset_itr.get());
 
@@ -770,7 +742,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> itr,
       language_segmenter->Segment("How are you你好吗お元気ですか"));
@@ -807,7 +780,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          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,
@@ -846,7 +820,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // CJKT (Chinese, Japanese, Khmer, Thai) are the 4 main languages that
   // don't have whitespaces as word delimiter. Chinese
   constexpr std::string_view kChinese = "我每天走路去上班。";
@@ -874,7 +849,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Japanese
   constexpr std::string_view kJapanese = "私は毎日仕事に歩いています。";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
@@ -901,7 +877,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kKhmer = "ញុំដើរទៅធ្វើការរាល់ថ្ងៃ។";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
                              language_segmenter->Segment(kKhmer));
@@ -927,7 +904,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Thai
   constexpr std::string_view kThai = "ฉันเดินไปทำงานทุกวัน";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
@@ -952,11 +930,13 @@
 
 TEST_P(ReverseJniLanguageSegmenterTest, ResetToTermBeforeWordConnectorUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "package name com:google:android!";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:      "package name com:google:android!"
   //               ^      ^^   ^^                 ^
@@ -975,11 +955,13 @@
 
 TEST_P(ReverseJniLanguageSegmenterTest, ResetToTermBeforeOutOfBoundsUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
-                             segmenter->Segment(kText));
+                             language_segmenter->Segment(kText));
 
   // String:      "How are you你好吗お元気ですか"
   //               ^  ^^  ^^  ^  ^ ^ ^  ^  ^
@@ -997,25 +979,27 @@
 }
 
 // Tests that ResetToTermBefore and Advance produce the same output. With the
-// exception of the last term which is inacessible via ResetToTermBefore,
-// the stream of terms produced by Advance calls should exacly match the
+// exception of the last term which is inaccessible via ResetToTermBefore,
+// the stream of terms produced by Advance calls should exactly match the
 // terms produced by ResetToTermBefore calls with the current position
 // provided as the argument (after their order has been reversed).
 TEST_P(ReverseJniLanguageSegmenterTest,
        MixedLanguagesResetToTermBeforeEquivalentToAdvanceUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kText = "How are𡔖 you你好吗お元気ですか";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kText));
+      language_segmenter->Segment(kText));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> reset_to_term_itr,
-      segmenter->Segment(kText));
+      language_segmenter->Segment(kText));
   std::vector<std::string_view> reset_terms =
       GetAllTermsResetBeforeUtf32(reset_to_term_itr.get());
   std::reverse(reset_terms.begin(), reset_terms.end());
@@ -1028,18 +1012,20 @@
 TEST_P(ReverseJniLanguageSegmenterTest,
        ThaiResetToTermBeforeEquivalentToAdvanceUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kThai = "ฉันเดินไปทำงานทุกวัน";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kThai));
+      language_segmenter->Segment(kThai));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> reset_to_term_itr,
-      segmenter->Segment(kThai));
+      language_segmenter->Segment(kThai));
   std::vector<std::string_view> reset_terms =
       GetAllTermsResetBeforeUtf32(reset_to_term_itr.get());
   std::reverse(reset_terms.begin(), reset_terms.end());
@@ -1051,18 +1037,20 @@
 TEST_P(ReverseJniLanguageSegmenterTest,
        KoreanResetToTermBeforeEquivalentToAdvanceUtf32) {
   ICING_ASSERT_OK_AND_ASSIGN(
-      auto segmenter, language_segmenter_factory::Create(
-                          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+      auto language_segmenter,
+      language_segmenter_factory::Create(
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kKorean = "나는 매일 출근합니다.";
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> advance_itr,
-      segmenter->Segment(kKorean));
+      language_segmenter->Segment(kKorean));
   std::vector<std::string_view> advance_terms =
       GetAllTermsAdvance(advance_itr.get());
 
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> reset_to_term_itr,
-      segmenter->Segment(kKorean));
+      language_segmenter->Segment(kKorean));
   std::vector<std::string_view> reset_terms =
       GetAllTermsResetBeforeUtf32(reset_to_term_itr.get());
   std::reverse(reset_terms.begin(), reset_terms.end());
@@ -1075,7 +1063,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<LanguageSegmenter::Iterator> itr,
       language_segmenter->Segment("How are you你好吗お元気ですか"));
@@ -1113,7 +1102,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          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,
@@ -1151,7 +1141,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // CJKT (Chinese, Japanese, Khmer, Thai) are the 4 main languages that
   // don't have whitespaces as word delimiter. Chinese
   constexpr std::string_view kChinese = "我每天走路去上班。";
@@ -1176,7 +1167,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Japanese
   constexpr std::string_view kJapanese = "私は毎日仕事に歩いています。";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
@@ -1200,7 +1192,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   constexpr std::string_view kKhmer = "ញុំដើរទៅធ្វើការរាល់ថ្ងៃ។";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
                              language_segmenter->Segment(kKhmer));
@@ -1223,7 +1216,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Thai
   constexpr std::string_view kThai = "ฉันเดินไปทำงานทุกวัน";
   ICING_ASSERT_OK_AND_ASSIGN(std::unique_ptr<LanguageSegmenter::Iterator> itr,
@@ -1250,7 +1244,8 @@
   ICING_ASSERT_OK_AND_ASSIGN(
       auto language_segmenter,
       language_segmenter_factory::Create(
-          GetSegmenterOptions(GetLocale(), jni_cache_.get())));
+          language_segmenter_factory::SegmenterOptions(
+              GetLocale(), jni_cache_.get())));
   // Validates that the input strings are not copied
   ICING_ASSERT_OK_AND_ASSIGN(
       std::vector<std::string_view> terms,
diff --git a/icing/tokenization/verbatim-tokenizer_test.cc b/icing/tokenization/verbatim-tokenizer_test.cc
index bae69ff..8e4418b 100644
--- a/icing/tokenization/verbatim-tokenizer_test.cc
+++ b/icing/tokenization/verbatim-tokenizer_test.cc
@@ -18,13 +18,13 @@
 #include "gtest/gtest.h"
 #include "icing/portable/platform.h"
 #include "icing/testing/common-matchers.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/jni-test-helpers.h"
 #include "icing/testing/test-data.h"
 #include "icing/tokenization/language-segmenter-factory.h"
 #include "icing/tokenization/token.h"
 #include "icing/tokenization/tokenizer-factory.h"
 #include "icing/util/character-iterator.h"
+#include "icing/util/icu-data-file-helper.h"
 #include "unicode/uloc.h"
 
 namespace icing {
@@ -40,7 +40,7 @@
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
 
diff --git a/icing/transform/icu/icu-normalizer.cc b/icing/transform/icu/icu-normalizer.cc
index 58d4956..74c240a 100644
--- a/icing/transform/icu/icu-normalizer.cc
+++ b/icing/transform/icu/icu-normalizer.cc
@@ -24,6 +24,7 @@
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/absl_ports/str_cat.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/character-iterator.h"
 #include "icing/util/i18n-utils.h"
 #include "icing/util/logging.h"
 #include "icing/util/status-macros.h"
@@ -121,8 +122,9 @@
     : term_transformer_(std::move(term_transformer)),
       max_term_byte_size_(max_term_byte_size) {}
 
-std::string IcuNormalizer::NormalizeTerm(const std::string_view term) const {
-  std::string normalized_text;
+Normalizer::NormalizedTerm IcuNormalizer::NormalizeTerm(
+    const std::string_view term) const {
+  NormalizedTerm normalized_text = {""};
 
   if (term.empty()) {
     return normalized_text;
@@ -138,17 +140,19 @@
   // Normalize the prefix that can be transformed into ASCII.
   // This is a faster method to normalize Latin terms.
   NormalizeLatinResult result = NormalizeLatin(normalizer2, term);
-  normalized_text = std::move(result.text);
+  normalized_text.text = std::move(result.text);
   if (result.end_pos < term.length()) {
     // Some portion of term couldn't be normalized via NormalizeLatin. Use
     // term_transformer to handle this portion.
     std::string_view rest_term = term.substr(result.end_pos);
-    absl_ports::StrAppend(&normalized_text,
-                          term_transformer_->Transform(rest_term));
+    TermTransformer::TransformResult transform_result =
+        term_transformer_->Transform(rest_term);
+    absl_ports::StrAppend(&normalized_text.text,
+                          transform_result.transformed_term);
   }
 
-  if (normalized_text.length() > max_term_byte_size_) {
-    i18n_utils::SafeTruncateUtf8(&normalized_text, max_term_byte_size_);
+  if (normalized_text.text.length() > max_term_byte_size_) {
+    i18n_utils::SafeTruncateUtf8(&normalized_text.text, max_term_byte_size_);
   }
 
   return normalized_text;
@@ -208,12 +212,12 @@
   }
 }
 
-std::string IcuNormalizer::TermTransformer::Transform(
-    const std::string_view term) const {
+IcuNormalizer::TermTransformer::TransformResult
+IcuNormalizer::TermTransformer::Transform(const std::string_view term) const {
   auto utf16_term_or = i18n_utils::Utf8ToUtf16(term);
   if (!utf16_term_or.ok()) {
     ICING_VLOG(0) << "Failed to convert UTF8 term '" << term << "' to UTF16";
-    return std::string(term);
+    return {std::string(term)};
   }
   std::u16string utf16_term = std::move(utf16_term_or).ValueOrDie();
   UErrorCode status = U_ZERO_ERROR;
@@ -248,15 +252,16 @@
   if (U_FAILURE(status)) {
     // Failed to transform, return its original form.
     ICING_LOG(WARNING) << "Failed to normalize UTF8 term: " << term;
-    return std::string(term);
+    return {std::string(term)};
   }
 
   auto utf8_term_or = i18n_utils::Utf16ToUtf8(utf16_term);
   if (!utf8_term_or.ok()) {
     ICING_VLOG(0) << "Failed to convert UTF16 term '" << term << "' to UTF8";
-    return std::string(term);
+    return {std::string(term)};
   }
-  return std::move(utf8_term_or).ValueOrDie();
+  std::string utf8_term = std::move(utf8_term_or).ValueOrDie();
+  return {std::move(utf8_term)};
 }
 
 bool IcuNormalizer::FindNormalizedLatinMatchEndPosition(
diff --git a/icing/transform/icu/icu-normalizer.h b/icing/transform/icu/icu-normalizer.h
index f6f2b78..c87f0bc 100644
--- a/icing/transform/icu/icu-normalizer.h
+++ b/icing/transform/icu/icu-normalizer.h
@@ -56,7 +56,8 @@
   //
   // NOTE: Term should not mix Latin and non-Latin characters. Doing so may
   // result in the non-Latin characters not properly being normalized
-  std::string NormalizeTerm(std::string_view term) const override;
+  Normalizer::NormalizedTerm NormalizeTerm(
+      std::string_view term) const override;
 
   // Returns a CharacterIterator pointing to one past the end of the segment of
   // term that (once normalized) matches with normalized_term.
@@ -86,7 +87,10 @@
     ~TermTransformer();
 
     // Transforms the text based on our rules described at top of this file
-    std::string Transform(std::string_view term) const;
+    struct TransformResult {
+      std::string transformed_term;
+    };
+    TransformResult Transform(std::string_view term) const;
 
     // Returns a CharacterIterator pointing to one past the end of the segment
     // of a non-latin term that (once normalized) matches with normalized_term.
diff --git a/icing/transform/icu/icu-normalizer_benchmark.cc b/icing/transform/icu/icu-normalizer_benchmark.cc
index 89d5f1e..d3d3074 100644
--- a/icing/transform/icu/icu-normalizer_benchmark.cc
+++ b/icing/transform/icu/icu-normalizer_benchmark.cc
@@ -15,10 +15,10 @@
 #include "testing/base/public/benchmark.h"
 #include "gmock/gmock.h"
 #include "icing/testing/common-matchers.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/test-data.h"
 #include "icing/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/icu-data-file-helper.h"
 
 // Run on a Linux workstation:
 //    $ blaze build -c opt --dynamic_mode=off --copt=-gmlt
@@ -54,7 +54,7 @@
 void BM_NormalizeUppercase(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
@@ -87,7 +87,7 @@
 void BM_NormalizeAccent(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
@@ -124,7 +124,7 @@
 void BM_NormalizeGreekAccent(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
@@ -161,7 +161,7 @@
 void BM_NormalizeHiragana(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
@@ -198,7 +198,7 @@
 void BM_UppercaseSubTokenLength(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
@@ -233,7 +233,7 @@
 void BM_AccentSubTokenLength(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
@@ -273,7 +273,7 @@
 void BM_HiraganaSubTokenLength(benchmark::State& state) {
   bool run_via_adb = absl::GetFlag(FLAGS_adb);
   if (!run_via_adb) {
-    ICING_ASSERT_OK(icu_data_file_helper::SetUpICUDataFile(
+    ICING_ASSERT_OK(icu_data_file_helper::SetUpIcuDataFile(
         GetTestFilePath("icing/icu.dat")));
   }
 
diff --git a/icing/transform/icu/icu-normalizer_test.cc b/icing/transform/icu/icu-normalizer_test.cc
index 0df23fc..7f5af04 100644
--- a/icing/transform/icu/icu-normalizer_test.cc
+++ b/icing/transform/icu/icu-normalizer_test.cc
@@ -17,11 +17,11 @@
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "icing/testing/common-matchers.h"
-#include "icing/testing/icu-data-file-helper.h"
 #include "icing/testing/icu-i18n-test-utils.h"
 #include "icing/testing/test-data.h"
 #include "icing/transform/normalizer-factory.h"
 #include "icing/transform/normalizer.h"
+#include "icing/util/icu-data-file-helper.h"
 
 namespace icing {
 namespace lib {
@@ -33,7 +33,7 @@
   void SetUp() override {
     ICING_ASSERT_OK(
         // File generated via icu_data_file rule in //icing/BUILD.
-        icu_data_file_helper::SetUpICUDataFile(
+        icu_data_file_helper::SetUpIcuDataFile(
             GetTestFilePath("icing/icu.dat")));
 
     ICING_ASSERT_OK_AND_ASSIGN(normalizer_, normalizer_factory::Create(
@@ -57,88 +57,109 @@
 
 // Strings that are already normalized won't change if normalized again.
 TEST_F(IcuNormalizerTest, AlreadyNormalized) {
-  EXPECT_THAT(normalizer_->NormalizeTerm(""), Eq(""));
-  EXPECT_THAT(normalizer_->NormalizeTerm("hello world"), Eq("hello world"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("你弽"), Eq("你弽"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("キャンパス"), Eq("キャンパス"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("안녕하세요"), Eq("안녕하세요"));
+  EXPECT_THAT(normalizer_->NormalizeTerm(""), EqualsNormalizedTerm(""));
+  EXPECT_THAT(normalizer_->NormalizeTerm("hello world"),
+              EqualsNormalizedTerm("hello world"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("キャンパス"),
+              EqualsNormalizedTerm("キャンパス"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("안녕하세요"),
+              EqualsNormalizedTerm("안녕하세요"));
 }
 
 TEST_F(IcuNormalizerTest, UppercaseToLowercase) {
-  EXPECT_THAT(normalizer_->NormalizeTerm("MDI"), Eq("mdi"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("Icing"), Eq("icing"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("MDI"), EqualsNormalizedTerm("mdi"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("Icing"),
+              EqualsNormalizedTerm("icing"));
 }
 
 TEST_F(IcuNormalizerTest, LatinLetterRemoveAccent) {
-  EXPECT_THAT(normalizer_->NormalizeTerm("Zürich"), Eq("zurich"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("après-midi"), Eq("apres-midi"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("Buenos días"), Eq("buenos dias"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("Zürich"),
+              EqualsNormalizedTerm("zurich"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("après-midi"),
+              EqualsNormalizedTerm("apres-midi"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("Buenos días"),
+              EqualsNormalizedTerm("buenos dias"));
   EXPECT_THAT(normalizer_->NormalizeTerm("ÀÁÂÃÄÅĀĂĄḀḁàáâãäåāăą"),
-              Eq("aaaaaaaaaaaaaaaaaaaa"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ḂḄḆḃḅḇ"), Eq("bbbbbb"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ÇĆĈĊČḈḉćĉċčç"), Eq("cccccccccccc"));
+              EqualsNormalizedTerm("aaaaaaaaaaaaaaaaaaaa"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ḂḄḆḃḅḇ"),
+              EqualsNormalizedTerm("bbbbbb"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ÇĆĈĊČḈḉćĉċčç"),
+              EqualsNormalizedTerm("cccccccccccc"));
   EXPECT_THAT(normalizer_->NormalizeTerm("ÐĎĐḊḌḎḐḒḋḍḏḑḓďđ"),
-              Eq("ddddddddddddddd"));
+              EqualsNormalizedTerm("ddddddddddddddd"));
   EXPECT_THAT(normalizer_->NormalizeTerm("ÈÉÊËĒĔĖĘḔḖḘḚḜḕḗḙḛḝèéêëēĕėęě"),
-              Eq("eeeeeeeeeeeeeeeeeeeeeeeeeee"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("Ḟḟ"), Eq("ff"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ĜĞĠĢḠḡĝğġģ"), Eq("gggggggggg"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ĤḢḤḦḨḪḣḥḧḩḫĥẖ"), Eq("hhhhhhhhhhhhh"));
+              EqualsNormalizedTerm("eeeeeeeeeeeeeeeeeeeeeeeeeee"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("Ḟḟ"), EqualsNormalizedTerm("ff"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ĜĞĠĢḠḡĝğġģ"),
+              EqualsNormalizedTerm("gggggggggg"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ĤḢḤḦḨḪḣḥḧḩḫĥẖ"),
+              EqualsNormalizedTerm("hhhhhhhhhhhhh"));
   EXPECT_THAT(normalizer_->NormalizeTerm("ÌÍÎÏĨĪďḏḭḯìíîïÄŠÄŤÄ­"),
-              Eq("iiiiiiiiiiiiiiiii"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("Ä´Äľ"), Eq("jj"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ĜḰḲḴḾḹḳġ"), Eq("kkkkkkkk"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ĚĝĽḜḸḟḡḚḝḽĺğĞ"), Eq("lllllllllllll"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ḾṀṂḿṁṃ"), Eq("mmmmmm"));
+              EqualsNormalizedTerm("iiiiiiiiiiiiiiiii"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("Ä´Äľ"), EqualsNormalizedTerm("jj"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ĜḰḲḴḾḹḳġ"),
+              EqualsNormalizedTerm("kkkkkkkk"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ĚĝĽḜḸḟḡḚḝḽĺğĞ"),
+              EqualsNormalizedTerm("lllllllllllll"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ḾṀṂḿṁṃ"),
+              EqualsNormalizedTerm("mmmmmm"));
   EXPECT_THAT(normalizer_->NormalizeTerm("ÑŃŅŇṄṆṈṊṅṇṉṋñńņň"),
-              Eq("nnnnnnnnnnnnnnnn"));
+              EqualsNormalizedTerm("nnnnnnnnnnnnnnnn"));
   EXPECT_THAT(normalizer_->NormalizeTerm("ŌŎŐÒÓÔÕÖṌṎṐṒṍṏṑṓòóôõöōŏő"),
-              Eq("oooooooooooooooooooooooo"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ṔṖṕṗ"), Eq("pppp"));
+              EqualsNormalizedTerm("oooooooooooooooooooooooo"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ṔṖṕṗ"), EqualsNormalizedTerm("pppp"));
   EXPECT_THAT(normalizer_->NormalizeTerm("ŔŖŘṘṚṜṞṙṛṝṟŕŗř"),
-              Eq("rrrrrrrrrrrrrr"));
+              EqualsNormalizedTerm("rrrrrrrrrrrrrr"));
   EXPECT_THAT(normalizer_->NormalizeTerm("ŚŜŞŠČ˜áš áš˘áš¤ášŚáš¨ášĄášŁášĽáš§ášŠČ™Ĺ›ĹĹŸš"),
-              Eq("ssssssssssssssssssss"));
+              EqualsNormalizedTerm("ssssssssssssssssssss"));
   EXPECT_THAT(normalizer_->NormalizeTerm("ŢŤȚṪṬṮṰṫṭṯṱțţť"),
-              Eq("tttttttttttttt"));
+              EqualsNormalizedTerm("tttttttttttttt"));
   EXPECT_THAT(normalizer_->NormalizeTerm("ŨŪŏÙÚÛÜᚲᚴ᚜ᚸᚺᚳᚾᚡᚚ᚝ùúûüĹŠĹŤĹ­"),
-              Eq("uuuuuuuuuuuuuuuuuuuuuuuu"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("᚟᚞ᚽᚿ"), Eq("vvvv"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ŴẀẂẄẆẈẁẃẅẇẉŵ"), Eq("wwwwwwwwwwww"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ẊẌẋẍ"), Eq("xxxx"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ÝĹśŸáşŽáşĹˇýÿ"), Eq("yyyyyyyy"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ŹŻŽẐẒẔẑẓẕźżž"), Eq("zzzzzzzzzzzz"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("Barış"), Eq("baris"));
+              EqualsNormalizedTerm("uuuuuuuuuuuuuuuuuuuuuuuu"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("᚟᚞ᚽᚿ"), EqualsNormalizedTerm("vvvv"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ŴẀẂẄẆẈẁẃẅẇẉŵ"),
+              EqualsNormalizedTerm("wwwwwwwwwwww"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ẊẌẋẍ"), EqualsNormalizedTerm("xxxx"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ÝĹśŸáşŽáşĹˇýÿ"),
+              EqualsNormalizedTerm("yyyyyyyy"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ŹŻŽẐẒẔẑẓẕźżž"),
+              EqualsNormalizedTerm("zzzzzzzzzzzz"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("Barış"),
+              EqualsNormalizedTerm("baris"));
 }
 
 TEST_F(IcuNormalizerTest, GreekLetterRemoveAccent) {
-  EXPECT_THAT(normalizer_->NormalizeTerm("kαλημέρα"), Eq("kαλημερα"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("εγγραφÎŽ"), Eq("εγγραφη"));
-  EXPECT_THAT(normalizer_->NormalizeTerm(
-                  "ἈἉἊἋἌἍἎἏᾈᾉᾊᾋᾌᾍᾎᾏᾸᾹᾺΆᾼἀἁἂἃἄἅἆἇὰάᾀᾁᾂᾃᾄᾅᾆᾇᾰᾱᾲᾳᾴᾶᾷ"),
-              Eq("αααααααααααααααααααααααααααααααααααααααααααααα"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("kαλημέρα"),
+              EqualsNormalizedTerm("kαλημερα"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("εγγραφÎŽ"),
+              EqualsNormalizedTerm("εγγραφη"));
+  EXPECT_THAT(
+      normalizer_->NormalizeTerm(
+          "ἈἉἊἋἌἍἎἏᾈᾉᾊᾋᾌᾍᾎᾏᾸᾹᾺΆᾼἀἁἂἃἄἅἆἇὰάᾀᾁᾂᾃᾄᾅᾆᾇᾰᾱᾲᾳᾴᾶᾷ"),
+      EqualsNormalizedTerm("αααααααααααααααααααααααααααααααααααααααααααααα"));
   EXPECT_THAT(normalizer_->NormalizeTerm("ἘἙἚἛἜἝῈΈἐἑἒἓἔἕὲέ"),
-              Eq("εεεεεεεεεεεεεεεε"));
+              EqualsNormalizedTerm("εεεεεεεεεεεεεεεε"));
   EXPECT_THAT(
       normalizer_->NormalizeTerm("ἨἩἪἫἬἭἮἯᾘᾙᾚᾛᾜᾝᾞᾟῊΉῌἠἡἢἣἤἥἦἧὴήᾐᾑᾒᾓᾔᾕᾖᾗῂῃῄῆῇ"),
-      Eq("ηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηη"));
+      EqualsNormalizedTerm("ηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηηη"));
   EXPECT_THAT(normalizer_->NormalizeTerm("ἸἹἺἻἼἽἾἿῘῙῚΊἰἱἲἳἴἵἶἷὶίῐῑῒΐῖῗ"),
-              Eq("ιιιιιιιιιιιιιιιιιιιιιιιιιιιι"));
+              EqualsNormalizedTerm("ιιιιιιιιιιιιιιιιιιιιιιιιιιιι"));
   EXPECT_THAT(normalizer_->NormalizeTerm("ὈὉὊὋὌὍῸΌὀὁὂὃὄὅὸό"),
-              Eq("οοοοοοοοοοοοοοοο"));
+              EqualsNormalizedTerm("οοοοοοοοοοοοοοοο"));
   EXPECT_THAT(normalizer_->NormalizeTerm("ὙὛὝὟῨῩῪΎὐὑὒὓὔὕὖὗὺύῠῡῢΰῦῧ"),
-              Eq("υυυυυυυυυυυυυυυυυυυυυυυυ"));
+              EqualsNormalizedTerm("υυυυυυυυυυυυυυυυυυυυυυυυ"));
   EXPECT_THAT(
       normalizer_->NormalizeTerm("ὨὊὪὍ὏Ὥ὎ὯឨដឪឍតឭណឯῺ῝῟ὠὥὢὣὤὼὌὧὟώហឥអឣឤូឌឧῲῳῴ῜ῡ"),
-      Eq("ωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωω"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("῏ῤῼ"), Eq("ρρρ"));
+      EqualsNormalizedTerm("ωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωωω"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("῏ῤῼ"), EqualsNormalizedTerm("ρρρ"));
 }
 
 // Accent / diacritic marks won't be removed in non-latin chars, e.g. in
 // Japanese
 TEST_F(IcuNormalizerTest, NonLatinLetterNotRemoveAccent) {
   // Katakana
-  EXPECT_THAT(normalizer_->NormalizeTerm("ダヂヅデド"), Eq("ダヂヅデド"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ダヂヅデド"),
+              EqualsNormalizedTerm("ダヂヅデド"));
 
   // Our current ICU rules can't handle Hebrew properly, e.g. the accents in
   // "אָלֶף־בֵּית עִבְרִי"
@@ -147,76 +168,98 @@
 
 TEST_F(IcuNormalizerTest, FullWidthCharsToASCII) {
   // Full-width punctuation to ASCII punctuation
-  EXPECT_THAT(normalizer_->NormalizeTerm("‘’.,!?:“”"), Eq("''.,!?:\"\""));
+  EXPECT_THAT(normalizer_->NormalizeTerm("‘’.,!?:“”"),
+              EqualsNormalizedTerm("''.,!?:\"\""));
   // Full-width 0-9
   EXPECT_THAT(normalizer_->NormalizeTerm("0123456789"),
-              Eq("0123456789"));
+              EqualsNormalizedTerm("0123456789"));
   // Full-width A-Z
   EXPECT_THAT(normalizer_->NormalizeTerm(
                   ""),
-              Eq("abcdefghijklmnopqrstuvwxyz"));
+              EqualsNormalizedTerm("abcdefghijklmnopqrstuvwxyz"));
   // Full-width a-z
   EXPECT_THAT(normalizer_->NormalizeTerm(
                   "abcdefghijklmnopqrstuvwxyz"),
-              Eq("abcdefghijklmnopqrstuvwxyz"));
+              EqualsNormalizedTerm("abcdefghijklmnopqrstuvwxyz"));
 }
 
 TEST_F(IcuNormalizerTest, IdeographicToASCII) {
   ICING_ASSERT_OK_AND_ASSIGN(auto normalizer, normalizer_factory::Create(
                                                   /*max_term_byte_size=*/1000));
 
-  EXPECT_THAT(normalizer->NormalizeTerm(",。"), Eq(",."));
+  EXPECT_THAT(normalizer->NormalizeTerm(",。"), EqualsNormalizedTerm(",."));
 }
 
 // For Katakana, each character is normalized to its full-width version.
 TEST_F(IcuNormalizerTest, KatakanaHalfWidthToFullWidth) {
-  EXPECT_THAT(normalizer_->NormalizeTerm("カ"), Eq("カ"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ォ"), Eq("ォ"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("サ"), Eq("サ"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ホ"), Eq("ホ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("カ"), EqualsNormalizedTerm("カ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ォ"), EqualsNormalizedTerm("ォ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("サ"), EqualsNormalizedTerm("サ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ホ"), EqualsNormalizedTerm("ホ"));
 }
 
 TEST_F(IcuNormalizerTest, HiraganaToKatakana) {
-  EXPECT_THAT(normalizer_->NormalizeTerm("あいうえお"), Eq("アイウエオ"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("かきくけこ"), Eq("カキクケコ"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("さしすせそ"), Eq("サシスセソ"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("たちつてと"), Eq("タチツテト"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("なにぬねの"), Eq("ナニヌネノ"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("はひふへほ"), Eq("ハヒフヘホ"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("まみむめも"), Eq("マミムメモ"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("やゆよ"), Eq("ヤユヨ"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("らりるれろ"), Eq("ラリルレロ"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("わゐゑを"), Eq("ワヰヱヲ"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ん"), Eq("ン"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("がぎぐげご"), Eq("ガギグゲゴ"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ざじずぜぞ"), Eq("ザジズゼゾ"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("だぢづでど"), Eq("ダヂヅデド"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ばびぶべぼ"), Eq("バビブベボ"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("ぱぴぷぺぽ"), Eq("パピプペポ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("あいうえお"),
+              EqualsNormalizedTerm("アイウエオ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("かきくけこ"),
+              EqualsNormalizedTerm("カキクケコ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("さしすせそ"),
+              EqualsNormalizedTerm("サシスセソ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("たちつてと"),
+              EqualsNormalizedTerm("タチツテト"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("なきくねぎ"),
+              EqualsNormalizedTerm("ナニヌネノ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("はひまへそ"),
+              EqualsNormalizedTerm("ハヒフヘホ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("まみむめも"),
+              EqualsNormalizedTerm("マミムメモ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("やゆよ"),
+              EqualsNormalizedTerm("ヤヌヨ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("らりるれろ"),
+              EqualsNormalizedTerm("ナリネハロ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("わゐゑを"),
+              EqualsNormalizedTerm("ワヰヹヲ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ん"), EqualsNormalizedTerm("ン"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("がぎぐげご"),
+              EqualsNormalizedTerm("ガギグゲゴ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ざじずぜぞ"),
+              EqualsNormalizedTerm("ザジズゼゾ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("だぢぼでお"),
+              EqualsNormalizedTerm("ダヂヅデド"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("ばびぜずた"),
+              EqualsNormalizedTerm("バビブベボ"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("べぴちぺぽ"),
+              EqualsNormalizedTerm("パピプペポ"));
+}
+
+TEST_F(IcuNormalizerTest, HanToPinyin) {
+  EXPECT_THAT(normalizer_->NormalizeTerm("戴"), EqualsNormalizedTerm("戴"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("慧"), EqualsNormalizedTerm("慧"));
 }
 
 TEST_F(IcuNormalizerTest, SuperscriptAndSubscriptToASCII) {
-  EXPECT_THAT(normalizer_->NormalizeTerm("⁚"), Eq("9"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("₉"), Eq("9"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("⁚"), EqualsNormalizedTerm("9"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("₉"), EqualsNormalizedTerm("9"));
 }
 
 TEST_F(IcuNormalizerTest, CircledCharsToASCII) {
-  EXPECT_THAT(normalizer_->NormalizeTerm("①"), Eq("1"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("Ⓐ"), Eq("a"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("①"), EqualsNormalizedTerm("1"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("Ⓐ"), EqualsNormalizedTerm("a"));
 }
 
 TEST_F(IcuNormalizerTest, RotatedCharsToASCII) {
-  EXPECT_THAT(normalizer_->NormalizeTerm("︡"), Eq("{"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("︸"), Eq("}"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("︡"), EqualsNormalizedTerm("{"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("︸"), EqualsNormalizedTerm("}"));
 }
 
 TEST_F(IcuNormalizerTest, SquaredCharsToASCII) {
-  EXPECT_THAT(normalizer_->NormalizeTerm("㌀"), Eq("アパート"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("㌀"),
+              EqualsNormalizedTerm("アパート"));
 }
 
 TEST_F(IcuNormalizerTest, FractionsToASCII) {
-  EXPECT_THAT(normalizer_->NormalizeTerm("¼"), Eq(" 1/4"));
-  EXPECT_THAT(normalizer_->NormalizeTerm("⅚"), Eq(" 5/6"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("¼"), EqualsNormalizedTerm(" 1/4"));
+  EXPECT_THAT(normalizer_->NormalizeTerm("⅚"), EqualsNormalizedTerm(" 5/6"));
 }
 
 TEST_F(IcuNormalizerTest, Truncate) {
@@ -225,19 +268,22 @@
                                                     /*max_term_byte_size=*/5));
 
     // Won't be truncated
-    EXPECT_THAT(normalizer->NormalizeTerm("hi"), Eq("hi"));
-    EXPECT_THAT(normalizer->NormalizeTerm("hello"), Eq("hello"));
+    EXPECT_THAT(normalizer->NormalizeTerm("hi"), EqualsNormalizedTerm("hi"));
+    EXPECT_THAT(normalizer->NormalizeTerm("hello"),
+                EqualsNormalizedTerm("hello"));
 
     // Truncated to length 5.
-    EXPECT_THAT(normalizer->NormalizeTerm("hello!"), Eq("hello"));
+    EXPECT_THAT(normalizer->NormalizeTerm("hello!"),
+                EqualsNormalizedTerm("hello"));
 
     // Each Japanese character has 3 bytes, so truncating to length 5 results in
     // only 1 character.
-    EXPECT_THAT(normalizer->NormalizeTerm("キャンパス"), Eq("キ"));
+    EXPECT_THAT(normalizer->NormalizeTerm("キャンパス"),
+                EqualsNormalizedTerm("キ"));
 
     // Each Greek character has 2 bytes, so truncating to length 5 results in 2
     // character.
-    EXPECT_THAT(normalizer->NormalizeTerm("αβγδε"), Eq("αβ"));
+    EXPECT_THAT(normalizer->NormalizeTerm("αβγδε"), EqualsNormalizedTerm("αβ"));
   }
 
   {
@@ -245,7 +291,7 @@
                                                     /*max_term_byte_size=*/2));
     // The Japanese character has 3 bytes, truncating it results in an empty
     // string.
-    EXPECT_THAT(normalizer->NormalizeTerm("キ"), Eq(""));
+    EXPECT_THAT(normalizer->NormalizeTerm("キ"), EqualsNormalizedTerm(""));
   }
 }
 
diff --git a/icing/transform/map/map-normalizer.cc b/icing/transform/map/map-normalizer.cc
index 61fce65..0c10cce 100644
--- a/icing/transform/map/map-normalizer.cc
+++ b/icing/transform/map/map-normalizer.cc
@@ -22,6 +22,7 @@
 
 #include "icing/absl_ports/str_cat.h"
 #include "icing/transform/map/normalization-map.h"
+#include "icing/transform/normalizer.h"
 #include "icing/util/character-iterator.h"
 #include "icing/util/i18n-utils.h"
 #include "icing/util/logging.h"
@@ -68,7 +69,8 @@
 
 }  // namespace
 
-std::string MapNormalizer::NormalizeTerm(std::string_view term) const {
+Normalizer::NormalizedTerm MapNormalizer::NormalizeTerm(
+    std::string_view term) const {
   std::string normalized_text;
   normalized_text.reserve(term.length());
 
@@ -101,7 +103,7 @@
     i18n_utils::SafeTruncateUtf8(&normalized_text, max_term_byte_size_);
   }
 
-  return normalized_text;
+  return {std::move(normalized_text)};
 }
 
 CharacterIterator MapNormalizer::FindNormalizedMatchEndPosition(
diff --git a/icing/transform/map/map-normalizer.h b/icing/transform/map/map-normalizer.h
index ed996ae..469d714 100644
--- a/icing/transform/map/map-normalizer.h
+++ b/icing/transform/map/map-normalizer.h
@@ -38,7 +38,8 @@
   //   - Common diacritic Latin characters -> ASCII
   //
   // Read more mapping details in normalization-map.cc
-  std::string NormalizeTerm(std::string_view term) const override;
+  Normalizer::NormalizedTerm NormalizeTerm(
+      std::string_view term) const override;
 
   // Returns a CharacterIterator pointing to one past the end of the segment of
   // term that (once normalized) matches with normalized_term.
diff --git a/icing/transform/map/map-normalizer_test.cc b/icing/transform/map/map-normalizer_test.cc
index adc5623..72b122c 100644
--- a/icing/transform/map/map-normalizer_test.cc
+++ b/icing/transform/map/map-normalizer_test.cc
@@ -48,63 +48,80 @@
   ICING_ASSERT_OK_AND_ASSIGN(auto normalizer, normalizer_factory::Create(
                                                   /*max_term_byte_size=*/1000));
 
-  EXPECT_THAT(normalizer->NormalizeTerm(""), Eq(""));
-  EXPECT_THAT(normalizer->NormalizeTerm("hello world"), Eq("hello world"));
-  EXPECT_THAT(normalizer->NormalizeTerm("你弽"), Eq("你弽"));
-  EXPECT_THAT(normalizer->NormalizeTerm("キャンパス"), Eq("キャンパス"));
-  EXPECT_THAT(normalizer->NormalizeTerm("안녕하세요"), Eq("안녕하세요"));
+  EXPECT_THAT(normalizer->NormalizeTerm(""), EqualsNormalizedTerm(""));
+  EXPECT_THAT(normalizer->NormalizeTerm("hello world"),
+              EqualsNormalizedTerm("hello world"));
+  EXPECT_THAT(normalizer->NormalizeTerm("你弽"), EqualsNormalizedTerm("你弽"));
+  EXPECT_THAT(normalizer->NormalizeTerm("キャンパス"),
+              EqualsNormalizedTerm("キャンパス"));
+  EXPECT_THAT(normalizer->NormalizeTerm("안녕하세요"),
+              EqualsNormalizedTerm("안녕하세요"));
 }
 
 TEST(MapNormalizerTest, UppercaseToLowercase) {
   ICING_ASSERT_OK_AND_ASSIGN(auto normalizer, normalizer_factory::Create(
                                                   /*max_term_byte_size=*/1000));
 
-  EXPECT_THAT(normalizer->NormalizeTerm("MDI"), Eq("mdi"));
-  EXPECT_THAT(normalizer->NormalizeTerm("Icing"), Eq("icing"));
+  EXPECT_THAT(normalizer->NormalizeTerm("MDI"), EqualsNormalizedTerm("mdi"));
+  EXPECT_THAT(normalizer->NormalizeTerm("Icing"),
+              EqualsNormalizedTerm("icing"));
 }
 
 TEST(MapNormalizerTest, LatinLetterRemoveAccent) {
   ICING_ASSERT_OK_AND_ASSIGN(auto normalizer, normalizer_factory::Create(
                                                   /*max_term_byte_size=*/1000));
 
-  EXPECT_THAT(normalizer->NormalizeTerm("Zürich"), Eq("zurich"));
-  EXPECT_THAT(normalizer->NormalizeTerm("après-midi"), Eq("apres-midi"));
-  EXPECT_THAT(normalizer->NormalizeTerm("Buenos días"), Eq("buenos dias"));
+  EXPECT_THAT(normalizer->NormalizeTerm("Zürich"),
+              EqualsNormalizedTerm("zurich"));
+  EXPECT_THAT(normalizer->NormalizeTerm("après-midi"),
+              EqualsNormalizedTerm("apres-midi"));
+  EXPECT_THAT(normalizer->NormalizeTerm("Buenos días"),
+              EqualsNormalizedTerm("buenos dias"));
   EXPECT_THAT(normalizer->NormalizeTerm("ÀÁÂÃÄÅĀĂĄḀḁàáâãäåāăą"),
-              Eq("aaaaaaaaaaaaaaaaaaaa"));
-  EXPECT_THAT(normalizer->NormalizeTerm("ḂḄḆḃḅḇ"), Eq("bbbbbb"));
-  EXPECT_THAT(normalizer->NormalizeTerm("ÇĆĈĊČḈḉćĉċčç"), Eq("cccccccccccc"));
+              EqualsNormalizedTerm("aaaaaaaaaaaaaaaaaaaa"));
+  EXPECT_THAT(normalizer->NormalizeTerm("ḂḄḆḃḅḇ"),
+              EqualsNormalizedTerm("bbbbbb"));
+  EXPECT_THAT(normalizer->NormalizeTerm("ÇĆĈĊČḈḉćĉċčç"),
+              EqualsNormalizedTerm("cccccccccccc"));
   EXPECT_THAT(normalizer->NormalizeTerm("ÐĎĐḊḌḎḐḒḋḍḏḑḓďđ"),
-              Eq("ddddddddddddddd"));
+              EqualsNormalizedTerm("ddddddddddddddd"));
   EXPECT_THAT(normalizer->NormalizeTerm("ÈÉÊËĒĔĖĘḔḖḘḚḜḕḗḙḛḝèéêëēĕėęě"),
-              Eq("eeeeeeeeeeeeeeeeeeeeeeeeeee"));
-  EXPECT_THAT(normalizer->NormalizeTerm("Ḟḟ"), Eq("ff"));
-  EXPECT_THAT(normalizer->NormalizeTerm("ĜĞĠĢḠḡĝğġģ"), Eq("gggggggggg"));
-  EXPECT_THAT(normalizer->NormalizeTerm("ĤḢḤḦḨḪḣḥḧḩḫĥẖ"), Eq("hhhhhhhhhhhhh"));
+              EqualsNormalizedTerm("eeeeeeeeeeeeeeeeeeeeeeeeeee"));
+  EXPECT_THAT(normalizer->NormalizeTerm("Ḟḟ"), EqualsNormalizedTerm("ff"));
+  EXPECT_THAT(normalizer->NormalizeTerm("ĜĞĠĢḠḡĝğġģ"),
+              EqualsNormalizedTerm("gggggggggg"));
+  EXPECT_THAT(normalizer->NormalizeTerm("ĤḢḤḦḨḪḣḥḧḩḫĥẖ"),
+              EqualsNormalizedTerm("hhhhhhhhhhhhh"));
   EXPECT_THAT(normalizer->NormalizeTerm("ÌÍÎÏĨĪďḏḭḯìíîïÄŠÄŤÄ­"),
-              Eq("iiiiiiiiiiiiiiiii"));
-  EXPECT_THAT(normalizer->NormalizeTerm("Ä´Äľ"), Eq("jj"));
-  EXPECT_THAT(normalizer->NormalizeTerm("ĜḰḲḴḾḹḳġ"), Eq("kkkkkkkk"));
-  EXPECT_THAT(normalizer->NormalizeTerm("ĚĝĽḜḸḟḡḚḝḽĺğĞ"), Eq("lllllllllllll"));
-  EXPECT_THAT(normalizer->NormalizeTerm("ḾṀṂḿṁṃ"), Eq("mmmmmm"));
+              EqualsNormalizedTerm("iiiiiiiiiiiiiiiii"));
+  EXPECT_THAT(normalizer->NormalizeTerm("Ä´Äľ"), EqualsNormalizedTerm("jj"));
+  EXPECT_THAT(normalizer->NormalizeTerm("ĜḰḲḴḾḹḳġ"),
+              EqualsNormalizedTerm("kkkkkkkk"));
+  EXPECT_THAT(normalizer->NormalizeTerm("ĚĝĽḜḸḟḡḚḝḽĺğĞ"),
+              EqualsNormalizedTerm("lllllllllllll"));
+  EXPECT_THAT(normalizer->NormalizeTerm("ḾṀṂḿṁṃ"),
+              EqualsNormalizedTerm("mmmmmm"));
   EXPECT_THAT(normalizer->NormalizeTerm("ÑŃŅŇṄṆṈṊṅṇṉṋñńņň"),
-              Eq("nnnnnnnnnnnnnnnn"));
+              EqualsNormalizedTerm("nnnnnnnnnnnnnnnn"));
   EXPECT_THAT(normalizer->NormalizeTerm("ŌŎŐÒÓÔÕÖṌṎṐṒṍṏṑṓòóôõöōŏő"),
-              Eq("oooooooooooooooooooooooo"));
-  EXPECT_THAT(normalizer->NormalizeTerm("ṔṖṕṗ"), Eq("pppp"));
+              EqualsNormalizedTerm("oooooooooooooooooooooooo"));
+  EXPECT_THAT(normalizer->NormalizeTerm("ṔṖṕṗ"), EqualsNormalizedTerm("pppp"));
   EXPECT_THAT(normalizer->NormalizeTerm("ŔŖŘṘṚṜṞṙṛṝṟŕŗř"),
-              Eq("rrrrrrrrrrrrrr"));
+              EqualsNormalizedTerm("rrrrrrrrrrrrrr"));
   EXPECT_THAT(normalizer->NormalizeTerm("ŚŜŞŠČ˜áš áš˘áš¤ášŚáš¨ášĄášŁášĽáš§ášŠČ™Ĺ›ĹĹŸš"),
-              Eq("ssssssssssssssssssss"));
+              EqualsNormalizedTerm("ssssssssssssssssssss"));
   EXPECT_THAT(normalizer->NormalizeTerm("ŢŤȚṪṬṮṰṫṭṯṱțţť"),
-              Eq("tttttttttttttt"));
+              EqualsNormalizedTerm("tttttttttttttt"));
   EXPECT_THAT(normalizer->NormalizeTerm("ŨŪŏÙÚÛÜᚲᚴ᚜ᚸᚺᚳᚾᚡᚚ᚝ùúûüĹŠĹŤĹ­"),
-              Eq("uuuuuuuuuuuuuuuuuuuuuuuu"));
-  EXPECT_THAT(normalizer->NormalizeTerm("᚟᚞ᚽᚿ"), Eq("vvvv"));
-  EXPECT_THAT(normalizer->NormalizeTerm("ŴẀẂẄẆẈẁẃẅẇẉŵ"), Eq("wwwwwwwwwwww"));
-  EXPECT_THAT(normalizer->NormalizeTerm("ẊẌẋẍ"), Eq("xxxx"));
-  EXPECT_THAT(normalizer->NormalizeTerm("ÝĹśŸáşŽáşĹˇýÿ"), Eq("yyyyyyyy"));
-  EXPECT_THAT(normalizer->NormalizeTerm("ŹŻŽẐẒẔẑẓẕźżž"), Eq("zzzzzzzzzzzz"));
+              EqualsNormalizedTerm("uuuuuuuuuuuuuuuuuuuuuuuu"));
+  EXPECT_THAT(normalizer->NormalizeTerm("᚟᚞ᚽᚿ"), EqualsNormalizedTerm("vvvv"));
+  EXPECT_THAT(normalizer->NormalizeTerm("ŴẀẂẄẆẈẁẃẅẇẉŵ"),
+              EqualsNormalizedTerm("wwwwwwwwwwww"));
+  EXPECT_THAT(normalizer->NormalizeTerm("ẊẌẋẍ"), EqualsNormalizedTerm("xxxx"));
+  EXPECT_THAT(normalizer->NormalizeTerm("ÝĹśŸáşŽáşĹˇýÿ"),
+              EqualsNormalizedTerm("yyyyyyyy"));
+  EXPECT_THAT(normalizer->NormalizeTerm("ŹŻŽẐẒẔẑẓẕźżž"),
+              EqualsNormalizedTerm("zzzzzzzzzzzz"));
 }
 
 // Accent / diacritic marks won't be removed in non-latin chars, e.g. in
@@ -114,12 +131,16 @@
                                                   /*max_term_byte_size=*/1000));
 
   // Katakana
-  EXPECT_THAT(normalizer->NormalizeTerm("ダヂヅデド"), Eq("ダヂヅデド"));
+  EXPECT_THAT(normalizer->NormalizeTerm("ダヂヅデド"),
+              EqualsNormalizedTerm("ダヂヅデド"));
   // Greek
-  EXPECT_THAT(normalizer->NormalizeTerm("kαλημέρα"), Eq("kαλημέρα"));
-  EXPECT_THAT(normalizer->NormalizeTerm("εγγραφÎŽ"), Eq("εγγραφÎŽ"));
+  EXPECT_THAT(normalizer->NormalizeTerm("kαλημέρα"),
+              EqualsNormalizedTerm("kαλημέρα"));
+  EXPECT_THAT(normalizer->NormalizeTerm("εγγραφÎŽ"),
+              EqualsNormalizedTerm("εγγραφÎŽ"));
   // Hebrew
-  EXPECT_THAT(normalizer->NormalizeTerm("אָלֶף־בֵּית עִבְרִי"), Eq("אָלֶף־בֵּית עִבְרִי"));
+  EXPECT_THAT(normalizer->NormalizeTerm("אָלֶף־בֵּית עִבְרִי"),
+              EqualsNormalizedTerm("אָלֶף־בֵּית עִבְרִי"));
 }
 
 TEST(MapNormalizerTest, FullWidthCharsToASCII) {
@@ -127,47 +148,63 @@
                                                   /*max_term_byte_size=*/1000));
 
   // Full-width punctuation to ASCII punctuation
-  EXPECT_THAT(normalizer->NormalizeTerm("‘’.,!?:“”"), Eq("''.,!?:\"\""));
+  EXPECT_THAT(normalizer->NormalizeTerm("‘’.,!?:“”"),
+              EqualsNormalizedTerm("''.,!?:\"\""));
   // Full-width 0-9
   EXPECT_THAT(normalizer->NormalizeTerm("0123456789"),
-              Eq("0123456789"));
+              EqualsNormalizedTerm("0123456789"));
   // Full-width A-Z
   EXPECT_THAT(normalizer->NormalizeTerm(
                   ""),
-              Eq("abcdefghijklmnopqrstuvwxyz"));
+              EqualsNormalizedTerm("abcdefghijklmnopqrstuvwxyz"));
   // Full-width a-z
   EXPECT_THAT(normalizer->NormalizeTerm(
                   "abcdefghijklmnopqrstuvwxyz"),
-              Eq("abcdefghijklmnopqrstuvwxyz"));
+              EqualsNormalizedTerm("abcdefghijklmnopqrstuvwxyz"));
 }
 
 TEST(MapNormalizerTest, IdeographicToASCII) {
   ICING_ASSERT_OK_AND_ASSIGN(auto normalizer, normalizer_factory::Create(
                                                   /*max_term_byte_size=*/1000));
 
-  EXPECT_THAT(normalizer->NormalizeTerm(",。"), Eq(",."));
+  EXPECT_THAT(normalizer->NormalizeTerm(",。"), EqualsNormalizedTerm(",."));
 }
 
 TEST(MapNormalizerTest, HiraganaToKatakana) {
   ICING_ASSERT_OK_AND_ASSIGN(auto normalizer, normalizer_factory::Create(
                                                   /*max_term_byte_size=*/1000));
 
-  EXPECT_THAT(normalizer->NormalizeTerm("あいうえお"), Eq("アイウエオ"));
-  EXPECT_THAT(normalizer->NormalizeTerm("かきくけこ"), Eq("カキクケコ"));
-  EXPECT_THAT(normalizer->NormalizeTerm("さしすせそ"), Eq("サシスセソ"));
-  EXPECT_THAT(normalizer->NormalizeTerm("たちつてと"), Eq("タチツテト"));
-  EXPECT_THAT(normalizer->NormalizeTerm("なにぬねの"), Eq("ナニヌネノ"));
-  EXPECT_THAT(normalizer->NormalizeTerm("はひふへほ"), Eq("ハヒフヘホ"));
-  EXPECT_THAT(normalizer->NormalizeTerm("まみむめも"), Eq("マミムメモ"));
-  EXPECT_THAT(normalizer->NormalizeTerm("やゆよ"), Eq("ヤユヨ"));
-  EXPECT_THAT(normalizer->NormalizeTerm("らりるれろ"), Eq("ラリルレロ"));
-  EXPECT_THAT(normalizer->NormalizeTerm("わゐゑを"), Eq("ワヰヱヲ"));
-  EXPECT_THAT(normalizer->NormalizeTerm("ん"), Eq("ン"));
-  EXPECT_THAT(normalizer->NormalizeTerm("がぎぐげご"), Eq("ガギグゲゴ"));
-  EXPECT_THAT(normalizer->NormalizeTerm("ざじずぜぞ"), Eq("ザジズゼゾ"));
-  EXPECT_THAT(normalizer->NormalizeTerm("だぢづでど"), Eq("ダヂヅデド"));
-  EXPECT_THAT(normalizer->NormalizeTerm("ばびぶべぼ"), Eq("バビブベボ"));
-  EXPECT_THAT(normalizer->NormalizeTerm("ぱぴぷぺぽ"), Eq("パピプペポ"));
+  EXPECT_THAT(normalizer->NormalizeTerm("あいうえお"),
+              EqualsNormalizedTerm("アイウエオ"));
+  EXPECT_THAT(normalizer->NormalizeTerm("かきくけこ"),
+              EqualsNormalizedTerm("カキクケコ"));
+  EXPECT_THAT(normalizer->NormalizeTerm("さしすせそ"),
+              EqualsNormalizedTerm("サシスセソ"));
+  EXPECT_THAT(normalizer->NormalizeTerm("たちつてと"),
+              EqualsNormalizedTerm("タチツテト"));
+  EXPECT_THAT(normalizer->NormalizeTerm("なきくねぎ"),
+              EqualsNormalizedTerm("ナニヌネノ"));
+  EXPECT_THAT(normalizer->NormalizeTerm("はひまへそ"),
+              EqualsNormalizedTerm("ハヒフヘホ"));
+  EXPECT_THAT(normalizer->NormalizeTerm("まみむめも"),
+              EqualsNormalizedTerm("マミムメモ"));
+  EXPECT_THAT(normalizer->NormalizeTerm("やゆよ"),
+              EqualsNormalizedTerm("ヤヌヨ"));
+  EXPECT_THAT(normalizer->NormalizeTerm("らりるれろ"),
+              EqualsNormalizedTerm("ナリネハロ"));
+  EXPECT_THAT(normalizer->NormalizeTerm("わゐゑを"),
+              EqualsNormalizedTerm("ワヰヹヲ"));
+  EXPECT_THAT(normalizer->NormalizeTerm("ん"), EqualsNormalizedTerm("ン"));
+  EXPECT_THAT(normalizer->NormalizeTerm("がぎぐげご"),
+              EqualsNormalizedTerm("ガギグゲゴ"));
+  EXPECT_THAT(normalizer->NormalizeTerm("ざじずぜぞ"),
+              EqualsNormalizedTerm("ザジズゼゾ"));
+  EXPECT_THAT(normalizer->NormalizeTerm("だぢぼでお"),
+              EqualsNormalizedTerm("ダヂヅデド"));
+  EXPECT_THAT(normalizer->NormalizeTerm("ばびぜずた"),
+              EqualsNormalizedTerm("バビブベボ"));
+  EXPECT_THAT(normalizer->NormalizeTerm("べぴちぺぽ"),
+              EqualsNormalizedTerm("パピプペポ"));
 }
 
 TEST(MapNormalizerTest, Truncate) {
@@ -176,19 +213,22 @@
                                                     /*max_term_byte_size=*/5));
 
     // Won't be truncated
-    EXPECT_THAT(normalizer->NormalizeTerm("hi"), Eq("hi"));
-    EXPECT_THAT(normalizer->NormalizeTerm("hello"), Eq("hello"));
+    EXPECT_THAT(normalizer->NormalizeTerm("hi"), EqualsNormalizedTerm("hi"));
+    EXPECT_THAT(normalizer->NormalizeTerm("hello"),
+                EqualsNormalizedTerm("hello"));
 
     // Truncated to length 5.
-    EXPECT_THAT(normalizer->NormalizeTerm("hello!"), Eq("hello"));
+    EXPECT_THAT(normalizer->NormalizeTerm("hello!"),
+                EqualsNormalizedTerm("hello"));
 
     // Each Japanese character has 3 bytes, so truncating to length 5 results in
     // only 1 character.
-    EXPECT_THAT(normalizer->NormalizeTerm("キャンパス"), Eq("キ"));
+    EXPECT_THAT(normalizer->NormalizeTerm("キャンパス"),
+                EqualsNormalizedTerm("キ"));
 
     // Each Greek character has 2 bytes, so truncating to length 5 results in 2
     // character.
-    EXPECT_THAT(normalizer->NormalizeTerm("αβγδε"), Eq("αβ"));
+    EXPECT_THAT(normalizer->NormalizeTerm("αβγδε"), EqualsNormalizedTerm("αβ"));
   }
 
   {
@@ -196,7 +236,7 @@
                                                     /*max_term_byte_size=*/2));
     // The Japanese character has 3 bytes, truncating it results in an empty
     // string.
-    EXPECT_THAT(normalizer->NormalizeTerm("キ"), Eq(""));
+    EXPECT_THAT(normalizer->NormalizeTerm("キ"), EqualsNormalizedTerm(""));
   }
 }
 
diff --git a/icing/transform/normalizer.h b/icing/transform/normalizer.h
index 2110f0f..0383f55 100644
--- a/icing/transform/normalizer.h
+++ b/icing/transform/normalizer.h
@@ -39,7 +39,10 @@
 
   // Normalizes the input term based on rules. See implementation classes for
   // specific transformation rules.
-  virtual std::string NormalizeTerm(std::string_view term) const = 0;
+  struct NormalizedTerm {
+    std::string text;
+  };
+  virtual NormalizedTerm NormalizeTerm(std::string_view term) const = 0;
 
   // Returns a CharacterIterator pointing to one past the end of the segment of
   // term that (once normalized) matches with normalized_term.
diff --git a/icing/util/document-validator_test.cc b/icing/util/document-validator_test.cc
index 2c366fd..2784522 100644
--- a/icing/util/document-validator_test.cc
+++ b/icing/util/document-validator_test.cc
@@ -23,6 +23,7 @@
 #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/proto/document.pb.h"
 #include "icing/proto/schema.pb.h"
@@ -30,6 +31,7 @@
 #include "icing/schema/schema-store.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
+#include "icing/testing/test-feature-flags.h"
 #include "icing/testing/tmp-directory.h"
 
 namespace icing {
@@ -61,6 +63,8 @@
   DocumentValidatorTest() {}
 
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+
     SchemaProto schema =
         SchemaBuilder()
             .AddType(
@@ -133,8 +137,8 @@
     schema_dir_ = GetTestTempDir() + "/schema_store";
     ASSERT_TRUE(filesystem_.CreateDirectory(schema_dir_.c_str()));
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_dir_,
+                                           &fake_clock_, feature_flags_.get()));
     ASSERT_THAT(schema_store_->SetSchema(
                     schema, /*ignore_errors_and_delete_documents=*/false,
                     /*allow_circular_schema_definitions=*/false),
@@ -182,6 +186,7 @@
                              SimpleEmailBuilder().Build());
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   std::string schema_dir_;
   Filesystem filesystem_;
   FakeClock fake_clock_;
@@ -488,7 +493,8 @@
   // Set a schema with only the 'Email' type
   ICING_ASSERT_OK_AND_ASSIGN(
       std::unique_ptr<SchemaStore> schema_store,
-      SchemaStore::Create(&filesystem_, custom_schema_dir, &fake_clock_));
+      SchemaStore::Create(&filesystem_, custom_schema_dir, &fake_clock_,
+                          feature_flags_.get()));
   ASSERT_THAT(schema_store->SetSchema(
                   email_schema, /*ignore_errors_and_delete_documents=*/false,
                   /*allow_circular_schema_definitions=*/false),
diff --git a/icing/util/encode-util.cc b/icing/util/encode-util.cc
index 32a2b09..c733117 100644
--- a/icing/util/encode-util.cc
+++ b/icing/util/encode-util.cc
@@ -44,7 +44,7 @@
   return value;
 }
 
-std::string EncodeStringToCString(std::string input) {
+std::string EncodeStringToCString(const std::string& input) {
   std::string encoded_str;
   // use uint32_t to store 4 bytes, using unsigned types to keep automatically
   // remove extra left most bits.
diff --git a/icing/util/encode-util.h b/icing/util/encode-util.h
index ca0d8a9..6e258b2 100644
--- a/icing/util/encode-util.h
+++ b/icing/util/encode-util.h
@@ -47,7 +47,7 @@
 // string.
 // Eg2: This increases the size from 1-byte string to 2-bytes C string.
 // Eg3: This increases the size from 2-byte string to 3-bytes C string.
-std::string EncodeStringToCString(std::string input);
+std::string EncodeStringToCString(const std::string& input);
 
 }  // namespace encode_util
 
diff --git a/icing/testing/icu-data-file-helper.cc b/icing/util/icu-data-file-helper.cc
similarity index 90%
rename from icing/testing/icu-data-file-helper.cc
rename to icing/util/icu-data-file-helper.cc
index aaeb738..a947e4d 100644
--- a/icing/testing/icu-data-file-helper.cc
+++ b/icing/util/icu-data-file-helper.cc
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#include "icing/testing/icu-data-file-helper.h"
+#include "icing/util/icu-data-file-helper.h"
 
 #include <sys/mman.h>
 
@@ -22,9 +22,10 @@
 #include "icing/text_classifier/lib3/utils/base/status.h"
 #include "icing/absl_ports/canonical_errors.h"
 #include "icing/file/filesystem.h"
+#ifdef CUSTOM_ICU_DAT_FILE
 #include "unicode/udata.h"
 #include "unicode/utypes.h"
-
+#endif  // CUSTOM_ICU_DAT_FILE
 namespace icing {
 namespace lib {
 
@@ -35,8 +36,9 @@
 // segmentation fault errors.
 bool has_set_up_icu_data_file = false;
 
-libtextclassifier3::Status SetUpICUDataFile(
+libtextclassifier3::Status SetUpIcuDataFile(
     const std::string& icu_data_file_absolute_path) {
+#ifdef CUSTOM_ICU_DAT_FILE
   if (has_set_up_icu_data_file) {
     return libtextclassifier3::Status::OK;
   }
@@ -62,7 +64,7 @@
   }
 
   has_set_up_icu_data_file = true;
-
+#endif  // CUSTOM_ICU_DAT_FILE
   return libtextclassifier3::Status::OK;
 }
 
diff --git a/icing/testing/icu-data-file-helper.h b/icing/util/icu-data-file-helper.h
similarity index 77%
rename from icing/testing/icu-data-file-helper.h
rename to icing/util/icu-data-file-helper.h
index d0276e7..695229a 100644
--- a/icing/testing/icu-data-file-helper.h
+++ b/icing/util/icu-data-file-helper.h
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef ICING_TESTING_ICU_DATA_FILE_HELPER
-#define ICING_TESTING_ICU_DATA_FILE_HELPER
+#ifndef ICING_UTIL_ICU_DATA_FILE_HELPER
+#define ICING_UTIL_ICU_DATA_FILE_HELPER
 
 #include "icing/text_classifier/lib3/utils/base/status.h"
 
@@ -22,8 +22,8 @@
 
 namespace icu_data_file_helper {
 
-// The library binary doesn't contain any ICU data files, so we generate a .dat
-// file at compile time and here make ICU use that file.
+// Initializes ICU using the specified absolute path to the ICU data file.
+// The data file can either be downloaded via MDD or generated at compile time.
 //
 // NOTE: This target does NOT contain the ICU .dat file. To use this helper
 // function, the calling target must include a data dependency on the .dat file
@@ -32,7 +32,7 @@
 // Returns:
 //   Ok on success
 //   INTERNAL_ERROR if failed on any errors
-libtextclassifier3::Status SetUpICUDataFile(
+libtextclassifier3::Status SetUpIcuDataFile(
     const std::string& icu_data_file_absolute_path);
 
 }  // namespace icu_data_file_helper
@@ -40,4 +40,4 @@
 }  // namespace lib
 }  // namespace icing
 
-#endif  // ICING_TESTING_ICU_DATA_FILE_HELPER
+#endif  // ICING_UTIL_ICU_DATA_FILE_HELPER
diff --git a/icing/util/math-util.h b/icing/util/math-util.h
index 3f2a69d..22f429f 100644
--- a/icing/util/math-util.h
+++ b/icing/util/math-util.h
@@ -15,6 +15,7 @@
 #ifndef ICING_UTIL_MATH_UTIL_H_
 #define ICING_UTIL_MATH_UTIL_H_
 
+#include <cstdint>
 #include <limits>
 
 namespace icing {
@@ -73,6 +74,23 @@
                           : (input_value - remainder + rounding_value);
 }
 
+// Returns the next power of 2 given n (the smallest power of 2 which is
+// greater or equal to n).
+//
+// REQUIRES: n <= 2^31, since 2^31 is the largest power of 2 that can be
+//   represented by an unsigned int.
+inline uint32_t NextPowerOf2(uint32_t n) {
+  if (n == 0) {
+    return 1;
+  }
+
+  if ((n & (n - 1)) != 0) {
+    // not 2's power
+    return UINT32_C(1) << (32 - __builtin_clz(n));
+  }
+  return n;
+}
+
 }  // namespace math_util
 
 }  // namespace lib
diff --git a/icing/util/math-util_test.cc b/icing/util/math-util_test.cc
new file mode 100644
index 0000000..ffc850e
--- /dev/null
+++ b/icing/util/math-util_test.cc
@@ -0,0 +1,53 @@
+// Copyright (C) 2024 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/math-util.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace icing {
+namespace lib {
+
+namespace {
+
+using ::testing::Eq;
+
+TEST(MathUtilTest, NextPowerOf2) {
+  EXPECT_THAT(math_util::NextPowerOf2(0), Eq(1));
+  EXPECT_THAT(math_util::NextPowerOf2(1), Eq(1));
+  EXPECT_THAT(math_util::NextPowerOf2(2), Eq(2));
+  EXPECT_THAT(math_util::NextPowerOf2(3), Eq(4));
+  EXPECT_THAT(math_util::NextPowerOf2(4), Eq(4));
+  EXPECT_THAT(math_util::NextPowerOf2(5), Eq(8));
+  EXPECT_THAT(math_util::NextPowerOf2(6), Eq(8));
+  EXPECT_THAT(math_util::NextPowerOf2(7), Eq(8));
+  EXPECT_THAT(math_util::NextPowerOf2(8), Eq(8));
+  EXPECT_THAT(math_util::NextPowerOf2(9), Eq(16));
+  EXPECT_THAT(math_util::NextPowerOf2(16), Eq(16));
+  EXPECT_THAT(math_util::NextPowerOf2(17), Eq(32));
+
+  // 2^31 - 1
+  EXPECT_THAT(math_util::NextPowerOf2((UINT32_C(1) << 31) - 1),
+              Eq(UINT32_C(1) << 31));
+
+  // 2^31
+  EXPECT_THAT(math_util::NextPowerOf2(UINT32_C(1) << 31),
+              Eq(UINT32_C(1) << 31));
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/util/scorable_property_set.cc b/icing/util/scorable_property_set.cc
new file mode 100644
index 0000000..2f7ac24
--- /dev/null
+++ b/icing/util/scorable_property_set.cc
@@ -0,0 +1,137 @@
+// Copyright (C) 2024 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/scorable_property_set.h"
+
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#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/proto/internal/scorable_property_set.pb.h"
+#include "icing/proto/schema.pb.h"
+#include "icing/schema/property-util.h"
+#include "icing/schema/schema-store.h"
+#include "icing/schema/scorable_property_manager.h"
+#include "icing/store/document-filter-data.h"
+#include "icing/util/status-macros.h"
+
+namespace icing {
+namespace lib {
+
+libtextclassifier3::StatusOr<std::unique_ptr<ScorablePropertySet>>
+ScorablePropertySet::Create(
+    ScorablePropertySetProto&& scorable_property_set_proto,
+    SchemaTypeId schema_type_id, const SchemaStore* schema_store) {
+  ICING_ASSIGN_OR_RETURN(
+      const std::vector<ScorablePropertyManager::ScorablePropertyInfo>*
+          ordered_scorable_property_info,
+      schema_store->GetOrderedScorablePropertyInfo(schema_type_id));
+  if (ordered_scorable_property_info == nullptr ||
+      ordered_scorable_property_info->size() !=
+          scorable_property_set_proto.properties_size()) {
+    return absl_ports::InvalidArgumentError(IcingStringUtil::StringPrintf(
+        "ScorablePropertySetProto data is inconsistent with the schema config "
+        "of type id %d",
+        schema_type_id));
+  }
+  return std::unique_ptr<ScorablePropertySet>(new ScorablePropertySet(
+      std::move(scorable_property_set_proto), schema_type_id, schema_store));
+}
+
+libtextclassifier3::StatusOr<std::unique_ptr<ScorablePropertySet>>
+ScorablePropertySet::Create(const DocumentProto& document,
+                            SchemaTypeId schema_type_id,
+                            const SchemaStore* schema_store) {
+  ICING_ASSIGN_OR_RETURN(
+      const std::vector<ScorablePropertyManager::ScorablePropertyInfo>*
+          ordered_scorable_property_info,
+      schema_store->GetOrderedScorablePropertyInfo(schema_type_id));
+  if (ordered_scorable_property_info == nullptr) {
+    // It should never happen
+    return absl_ports::InternalError(
+        "SchemaStore::ordered_scorable_property_paths returned nullptr");
+  }
+  if (ordered_scorable_property_info->empty()) {
+    return absl_ports::InvalidArgumentError(IcingStringUtil::StringPrintf(
+        "No scorable property defined under the config of type id %d",
+        schema_type_id));
+  }
+
+  ScorablePropertySetProto scorable_property_set_proto_to_build;
+  for (const ScorablePropertyManager::ScorablePropertyInfo&
+           scorable_property_info : *ordered_scorable_property_info) {
+    ScorablePropertyProto* new_property =
+        scorable_property_set_proto_to_build.add_properties();
+
+    if (scorable_property_info.data_type ==
+        PropertyConfigProto::DataType::DOUBLE) {
+      libtextclassifier3::StatusOr<std::vector<double>> content_or =
+          property_util::ExtractPropertyValuesFromDocument<double>(
+              document, scorable_property_info.property_path);
+      if (content_or.ok()) {
+        new_property->mutable_double_values()->Add(
+            content_or.ValueOrDie().begin(), content_or.ValueOrDie().end());
+      }
+    } else if (scorable_property_info.data_type ==
+               PropertyConfigProto::DataType::INT64) {
+      libtextclassifier3::StatusOr<std::vector<int64_t>> content_or =
+          property_util::ExtractPropertyValuesFromDocument<int64_t>(
+              document, scorable_property_info.property_path);
+      if (content_or.ok()) {
+        new_property->mutable_int64_values()->Add(
+            content_or.ValueOrDie().begin(), content_or.ValueOrDie().end());
+      }
+    } else if (scorable_property_info.data_type ==
+               PropertyConfigProto::DataType::BOOLEAN) {
+      libtextclassifier3::StatusOr<std::vector<bool>> content_or =
+          property_util::ExtractPropertyValuesFromDocument<bool>(
+              document, scorable_property_info.property_path);
+      if (content_or.ok()) {
+        new_property->mutable_boolean_values()->Add(
+            content_or.ValueOrDie().begin(), content_or.ValueOrDie().end());
+      }
+    }
+  }
+  return std::unique_ptr<ScorablePropertySet>(
+      new ScorablePropertySet(std::move(scorable_property_set_proto_to_build),
+                              schema_type_id, schema_store));
+}
+
+const ScorablePropertyProto* ScorablePropertySet::GetScorablePropertyProto(
+    const std::string& property_path) const {
+  libtextclassifier3::StatusOr<std::optional<int>> index_or =
+      schema_store_->GetScorablePropertyIndex(schema_type_id_, property_path);
+  if (!index_or.ok() || !index_or.ValueOrDie().has_value()) {
+    return nullptr;
+  }
+  return &scorable_property_set_proto_.properties(
+      index_or.ValueOrDie().value());
+}
+
+ScorablePropertySet::ScorablePropertySet(
+    ScorablePropertySetProto&& scorable_property_set_proto,
+    SchemaTypeId schema_type_id, const SchemaStore* schema_store)
+    : scorable_property_set_proto_(std::move(scorable_property_set_proto)),
+      schema_type_id_(schema_type_id),
+      schema_store_(schema_store) {}
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/util/scorable_property_set.h b/icing/util/scorable_property_set.h
new file mode 100644
index 0000000..a11919f
--- /dev/null
+++ b/icing/util/scorable_property_set.h
@@ -0,0 +1,94 @@
+// Copyright (C) 2024 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_SCORABLE_PROPERTY_SET_H_
+#define ICING_UTIL_SCORABLE_PROPERTY_SET_H_
+
+#include <memory>
+#include <string>
+
+#include "icing/text_classifier/lib3/utils/base/statusor.h"
+#include "icing/proto/internal/scorable_property_set.pb.h"
+#include "icing/proto/schema.pb.h"
+#include "icing/schema/schema-store.h"
+#include "icing/store/document-filter-data.h"
+
+namespace icing {
+namespace lib {
+
+// A class that interprets and represents the data from the proto of
+// ScorablePropertySetProto.
+//
+// This class serves as an utility to access the scorable property data.
+class ScorablePropertySet {
+ public:
+  // Creates a class that represents the data from the proto of
+  // ScorablePropertySetProto.
+  //
+  // This function is light weight, and it doesn't perform any proto copy.
+  //
+  // Returns:
+  //   - INVALID_ARGUMENT if |schema_type_id| is invalid, or the proto data is
+  //     inconsistent with the schema config.
+  //   - FAILED_PRECONDITION if the schema hasn't been set yet
+  static libtextclassifier3::StatusOr<std::unique_ptr<ScorablePropertySet>>
+  Create(ScorablePropertySetProto&& scorable_property_set_proto,
+         SchemaTypeId schema_type_id, const SchemaStore* schema_store);
+
+  // Creates a class that represents the data from the proto of
+  // ScorablePropertySetProto.
+  //
+  // This function converts the input |document| to the ScorablePropertySetProto
+  // under the hood.
+  //
+  // Returns:
+  //   - INVALID_ARGUMENT if |schema_type_id| is invalid, or that no scorable
+  //     property is found under the schema config of |schema_type_id|.
+  //   - FAILED_PRECONDITION if the schema hasn't been set yet
+  static libtextclassifier3::StatusOr<std::unique_ptr<ScorablePropertySet>>
+  Create(const DocumentProto& document, SchemaTypeId schema_type_id,
+         const SchemaStore* schema_store);
+
+  // Delete copy constructor and assignment operator.
+  ScorablePropertySet(const ScorablePropertySet&) = delete;
+  ScorablePropertySet& operator=(const ScorablePropertySet&) = delete;
+
+  // Returns the scorable property proto for the given property path.
+  //
+  // Return:
+  //   - ScorablePropertyProto on success
+  //   - nullptr if the property path is not found in the schema config
+  //     as a scorable property.
+  const ScorablePropertyProto* GetScorablePropertyProto(
+      const std::string& property_path) const;
+
+  // Returns the reference to the underlying ScorablePropertySetProto.
+  const ScorablePropertySetProto& GetScorablePropertySetProto() const {
+    return scorable_property_set_proto_;
+  }
+
+ private:
+  explicit ScorablePropertySet(
+      ScorablePropertySetProto&& scorable_property_set_proto,
+      SchemaTypeId schema_type_id, const SchemaStore* schema_store);
+
+  const ScorablePropertySetProto scorable_property_set_proto_;
+  const SchemaTypeId schema_type_id_;
+  const SchemaStore* schema_store_;
+};
+
+}  // namespace lib
+}  // namespace icing
+
+#endif  // ICING_UTIL_SCORABLE_PROPERTY_SET_H_
diff --git a/icing/util/scorable_property_set_test.cc b/icing/util/scorable_property_set_test.cc
new file mode 100644
index 0000000..1dc9f64
--- /dev/null
+++ b/icing/util/scorable_property_set_test.cc
@@ -0,0 +1,422 @@
+// Copyright (C) 2024 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/scorable_property_set.h"
+
+#include <cstdint>
+#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/proto/internal/scorable_property_set.pb.h"
+#include "icing/proto/schema.pb.h"
+#include "icing/schema-builder.h"
+#include "icing/schema/schema-store.h"
+#include "icing/store/document-filter-data.h"
+#include "icing/testing/common-matchers.h"
+#include "icing/testing/fake-clock.h"
+#include "icing/testing/test-feature-flags.h"
+#include "icing/testing/tmp-directory.h"
+
+namespace icing {
+namespace lib {
+
+namespace {
+
+using ::icing::lib::portable_equals_proto::EqualsProto;
+using ::testing::Pointee;
+
+ScorablePropertyProto BuildScorablePropertyProtoFromBoolean(
+    const std::vector<bool>& boolean_values) {
+  ScorablePropertyProto scorable_property;
+  scorable_property.mutable_boolean_values()->Add(boolean_values.begin(),
+                                                  boolean_values.end());
+  return scorable_property;
+}
+
+ScorablePropertyProto BuildScorablePropertyProtoFromInt64(
+    const std::vector<int64_t>& int64_values) {
+  ScorablePropertyProto scorable_property;
+  scorable_property.mutable_int64_values()->Add(int64_values.begin(),
+                                                int64_values.end());
+  return scorable_property;
+}
+
+ScorablePropertyProto BuildScorablePropertyProtoFromDouble(
+    const std::vector<double>& double_values) {
+  ScorablePropertyProto scorable_property;
+  scorable_property.mutable_double_values()->Add(double_values.begin(),
+                                                 double_values.end());
+  return scorable_property;
+}
+
+class ScorablePropertySetTest : public ::testing::Test {
+ protected:
+  ScorablePropertySetTest()
+      : schema_store_dir_(GetTestTempDir() + "/schema_store") {}
+
+  void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
+    filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str());
+
+    ICING_ASSERT_OK_AND_ASSIGN(
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
+
+    SchemaProto schema_proto =
+        SchemaBuilder()
+            .AddType(SchemaTypeConfigBuilder().SetType("dummy").AddProperty(
+                PropertyConfigBuilder()
+                    .SetName("id")
+                    .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                    .SetCardinality(CARDINALITY_REPEATED)))
+            .AddType(
+                SchemaTypeConfigBuilder()
+                    .SetType("person")
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("id")
+                                     .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                     .SetCardinality(CARDINALITY_REPEATED))
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("income")
+                            .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                            .SetScorableType(SCORABLE_TYPE_ENABLED)
+                            .SetCardinality(CARDINALITY_REPEATED))
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("age")
+                            .SetDataType(PropertyConfigProto::DataType::INT64)
+                            .SetScorableType(SCORABLE_TYPE_ENABLED)
+                            .SetCardinality(CARDINALITY_OPTIONAL))
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("isStarred")
+                            .SetDataType(PropertyConfigProto::DataType::BOOLEAN)
+                            .SetScorableType(SCORABLE_TYPE_ENABLED)
+                            .SetCardinality(CARDINALITY_OPTIONAL)))
+            .AddType(
+                SchemaTypeConfigBuilder()
+                    .SetType("email")
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("subject")
+                                     .SetDataTypeString(TERM_MATCH_EXACT,
+                                                        TOKENIZER_PLAIN)
+                                     .SetCardinality(CARDINALITY_OPTIONAL))
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("sender")
+                            .SetDataTypeDocument(
+                                "person", /*index_nested_properties=*/true)
+                            .SetCardinality(CARDINALITY_OPTIONAL))
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("receiver")
+                            .SetDataTypeDocument(
+                                "person", /*index_nested_properties=*/true)
+                            .SetCardinality(CARDINALITY_REPEATED))
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("importanceBoolean")
+                            .SetDataType(PropertyConfigProto::DataType::BOOLEAN)
+                            .SetScorableType(SCORABLE_TYPE_ENABLED)
+                            .SetCardinality(CARDINALITY_OPTIONAL))
+                    .AddProperty(
+                        PropertyConfigBuilder()
+                            .SetName("scoreDouble")
+                            .SetDataType(PropertyConfigProto::DataType::DOUBLE)
+                            .SetScorableType(SCORABLE_TYPE_ENABLED)
+                            .SetCardinality(CARDINALITY_REPEATED))
+                    .AddProperty(PropertyConfigBuilder()
+                                     .SetName("scoreInt64")
+                                     .SetScorableType(SCORABLE_TYPE_ENABLED)
+                                     .SetDataTypeInt64(NUMERIC_MATCH_RANGE)
+                                     .SetCardinality(CARDINALITY_REPEATED)))
+            .Build();
+    ICING_ASSERT_OK(schema_store_->SetSchema(
+        schema_proto, /*ignore_errors_and_delete_documents=*/false,
+        /*allow_circular_schema_definitions=*/false));
+    email_schema_type_id_ =
+        schema_store_->GetSchemaTypeId("email").ValueOrDie();
+    person_schema_type_id_ =
+        schema_store_->GetSchemaTypeId("person").ValueOrDie();
+    dummy_schema_type_id_ =
+        schema_store_->GetSchemaTypeId("dummy").ValueOrDie();
+  }
+
+  void TearDown() override {
+    schema_store_.reset();
+    filesystem_.DeleteDirectoryRecursively(schema_store_dir_.c_str());
+  }
+
+  std::unique_ptr<FeatureFlags> feature_flags_;
+  Filesystem filesystem_;
+  std::string schema_store_dir_;
+  FakeClock fake_clock_;
+  SchemaTypeId email_schema_type_id_;
+  SchemaTypeId person_schema_type_id_;
+  SchemaTypeId dummy_schema_type_id_;
+  std::unique_ptr<SchemaStore> schema_store_;
+};
+
+TEST_F(ScorablePropertySetTest,
+       BuildFromScorablePropertySetProto_GetScorablePropertyProto) {
+  ScorablePropertyProto is_starred_scorable_property =
+      BuildScorablePropertyProtoFromBoolean({true});
+  ScorablePropertyProto income_scorable_property =
+      BuildScorablePropertyProtoFromDouble({1.5, 2.5});
+  ScorablePropertyProto age_scorable_property =
+      BuildScorablePropertyProtoFromInt64({45});
+
+  ScorablePropertySetProto scorable_property_set_proto;
+  *scorable_property_set_proto.add_properties() = age_scorable_property;
+  *scorable_property_set_proto.add_properties() = income_scorable_property;
+  *scorable_property_set_proto.add_properties() = is_starred_scorable_property;
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<ScorablePropertySet> scorable_property_set,
+      ScorablePropertySet::Create(std::move(scorable_property_set_proto),
+                                  person_schema_type_id_, schema_store_.get()));
+  EXPECT_THAT(scorable_property_set->GetScorablePropertyProto("age"),
+              Pointee(EqualsProto(age_scorable_property)));
+  EXPECT_THAT(scorable_property_set->GetScorablePropertyProto("income"),
+              Pointee(EqualsProto(income_scorable_property)));
+  EXPECT_THAT(scorable_property_set->GetScorablePropertyProto("isStarred"),
+              Pointee(EqualsProto(is_starred_scorable_property)));
+
+  EXPECT_EQ(
+      scorable_property_set->GetScorablePropertyProto("non_exist_property"),
+      nullptr);
+
+  EXPECT_EQ(scorable_property_set->GetScorablePropertyProto("subject"),
+            nullptr);
+}
+
+TEST_F(ScorablePropertySetTest,
+       BuildFromScorablePropertySetProto_InconsistentWithSchemaConfig) {
+  ScorablePropertyProto scorable_property_proto_boolean =
+      BuildScorablePropertyProtoFromBoolean({true});
+
+  ScorablePropertySetProto scorable_property_set_proto;
+  *scorable_property_set_proto.add_properties() =
+      scorable_property_proto_boolean;
+  EXPECT_THAT(
+      ScorablePropertySet::Create(std::move(scorable_property_set_proto),
+                                  email_schema_type_id_, schema_store_.get()),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+TEST_F(ScorablePropertySetTest,
+       BuildFromScorablePropertySetProto_InvalidSchemaTypeId) {
+  ScorablePropertyProto scorable_property_proto_boolean =
+      BuildScorablePropertyProtoFromBoolean({true});
+
+  ScorablePropertySetProto scorable_property_set_proto;
+  *scorable_property_set_proto.add_properties() =
+      scorable_property_proto_boolean;
+  EXPECT_THAT(
+      ScorablePropertySet::Create(std::move(scorable_property_set_proto),
+                                  /*schema_type_id=*/1000, schema_store_.get()),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+TEST_F(ScorablePropertySetTest,
+       BuildFromScorablePropertySetProto_WithSomePropertiesNotPopulated) {
+  ScorablePropertyProto age_scorable_property;
+  ScorablePropertyProto is_starred_scorable_property;
+  ScorablePropertyProto income_scorable_property =
+      BuildScorablePropertyProtoFromInt64({1, 2, 3});
+
+  ScorablePropertySetProto scorable_property_set_proto;
+  *scorable_property_set_proto.add_properties() = age_scorable_property;
+  *scorable_property_set_proto.add_properties() = income_scorable_property;
+  *scorable_property_set_proto.add_properties() = is_starred_scorable_property;
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<ScorablePropertySet> scorable_property_set,
+      ScorablePropertySet::Create(std::move(scorable_property_set_proto),
+                                  person_schema_type_id_, schema_store_.get()));
+  EXPECT_THAT(scorable_property_set->GetScorablePropertyProto("age"),
+              Pointee(EqualsProto(age_scorable_property)));
+  EXPECT_THAT(scorable_property_set->GetScorablePropertyProto("isStarred"),
+              Pointee(EqualsProto(is_starred_scorable_property)));
+  EXPECT_THAT(scorable_property_set->GetScorablePropertyProto("income"),
+              Pointee(EqualsProto(income_scorable_property)));
+}
+
+TEST_F(ScorablePropertySetTest, BuildFromDocument_InvalidSchemaTypeId) {
+  DocumentProto document =
+      DocumentBuilder()
+          .SetKey("foo", "1")
+          .SetSchema("email")
+          .AddStringProperty("subjectString", "subject foo")
+          .SetCreationTimestampMs(0)
+          .Build();
+
+  EXPECT_THAT(
+      ScorablePropertySet::Create(document,
+                                  /*schema_type_id=*/1000, schema_store_.get()),
+      StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+TEST_F(ScorablePropertySetTest,
+       BuildFromDocument_NoScorablePropertiesFromDocument) {
+  DocumentProto document =
+      DocumentBuilder()
+          .SetKey("foo", "1")
+          .SetSchema("email")
+          .AddStringProperty("subjectString", "subject foo")
+          .SetCreationTimestampMs(0)
+          .Build();
+
+  ScorablePropertySetProto expected_scorable_property_set;
+  expected_scorable_property_set.add_properties();  // importanceBoolean
+  expected_scorable_property_set.add_properties();  // receiver.age
+  expected_scorable_property_set.add_properties();  // receiver.income
+  expected_scorable_property_set.add_properties();  // receiver.isStarred
+  expected_scorable_property_set.add_properties();  // scoreDouble
+  expected_scorable_property_set.add_properties();  // scoreInt64
+  expected_scorable_property_set.add_properties();  // sender.age
+  expected_scorable_property_set.add_properties();  // sender.income
+  expected_scorable_property_set.add_properties();  // sender.isStarred
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<ScorablePropertySet> scorable_property_set,
+      ScorablePropertySet::Create(document, email_schema_type_id_,
+                                  schema_store_.get()));
+  EXPECT_THAT(scorable_property_set->GetScorablePropertySetProto(),
+              EqualsProto(expected_scorable_property_set));
+}
+
+TEST_F(ScorablePropertySetTest,
+       BuildFromDocument_NoScorablePropertiesFromSchema) {
+  DocumentProto document;
+  EXPECT_THAT(ScorablePropertySet::Create(document, dummy_schema_type_id_,
+                                          schema_store_.get()),
+              StatusIs(libtextclassifier3::StatusCode::INVALID_ARGUMENT));
+}
+
+TEST_F(ScorablePropertySetTest, ScorablePropertySetFromNestedDocument) {
+  DocumentProto document =
+      DocumentBuilder()
+          .SetKey("foo", "1")
+          .SetSchema("email")
+          .AddStringProperty("subjectString", "subject foo")
+          .AddBooleanProperty("importanceBoolean", true)
+          .AddInt64Property("scoreInt64", 1, 2, 3)
+          .AddDocumentProperty(
+              "receiver",
+              DocumentBuilder()
+                  .SetKey("namespace", "uri1")
+                  .SetSchema("person")
+                  .AddInt64Property("age", 30)
+                  .AddDoubleProperty("income", 10000, 20000, 30000)
+                  .AddBooleanProperty("isStarred", true)
+                  .Build(),
+              DocumentBuilder()
+                  .SetKey("namespace", "uri2")
+                  .SetSchema("person")
+                  .AddInt64Property("age", 35)
+                  .AddDoubleProperty("income", 10001, 20001, 30001)
+                  .AddBooleanProperty("isStarred", false)
+                  .Build())
+          .AddDocumentProperty(
+              "sender", DocumentBuilder()
+                            .SetKey("namespace", "uri3")
+                            .SetSchema("person")
+                            .AddInt64Property("age", 50)
+                            .AddDoubleProperty("income", 21001, 21002, 21003)
+                            .AddBooleanProperty("isStarred", false)
+                            .Build())
+          .SetCreationTimestampMs(0)
+          .Build();
+
+  ScorablePropertySetProto expected_scorable_property_set;
+  // importanceBoolean
+  *expected_scorable_property_set.add_properties() =
+      BuildScorablePropertyProtoFromBoolean({true});
+  // receiver.age
+  *expected_scorable_property_set.add_properties() =
+      BuildScorablePropertyProtoFromInt64({30, 35});
+  // receiver.income
+  *expected_scorable_property_set.add_properties() =
+      BuildScorablePropertyProtoFromDouble(
+          {10000, 20000, 30000, 10001, 20001, 30001});
+  // receiver.isStarred
+  *expected_scorable_property_set.add_properties() =
+      BuildScorablePropertyProtoFromBoolean({true, false});
+  // scoreDouble
+  *expected_scorable_property_set.add_properties() =
+      BuildScorablePropertyProtoFromDouble({});
+  // scoreInt64
+  *expected_scorable_property_set.add_properties() =
+      BuildScorablePropertyProtoFromInt64({1, 2, 3});
+  // sender.age
+  *expected_scorable_property_set.add_properties() =
+      BuildScorablePropertyProtoFromInt64({50});
+  // sender.income
+  *expected_scorable_property_set.add_properties() =
+      BuildScorablePropertyProtoFromDouble({21001, 21002, 21003});
+  // sender.isStarred
+  *expected_scorable_property_set.add_properties() =
+      BuildScorablePropertyProtoFromBoolean({false});
+
+  ICING_ASSERT_OK_AND_ASSIGN(
+      std::unique_ptr<ScorablePropertySet> scorable_property_set,
+      ScorablePropertySet::Create(document, email_schema_type_id_,
+                                  schema_store_.get()));
+  EXPECT_THAT(scorable_property_set->GetScorablePropertySetProto(),
+              EqualsProto(expected_scorable_property_set));
+
+  // Test GetScorablePropertyProto() for each property path.
+  EXPECT_THAT(
+      scorable_property_set->GetScorablePropertyProto("importanceBoolean"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromBoolean({true}))));
+  EXPECT_THAT(scorable_property_set->GetScorablePropertyProto("scoreDouble"),
+              Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble({}))));
+  EXPECT_THAT(
+      scorable_property_set->GetScorablePropertyProto("scoreInt64"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromInt64({1, 2, 3}))));
+  EXPECT_THAT(scorable_property_set->GetScorablePropertyProto("sender.age"),
+              Pointee(EqualsProto(BuildScorablePropertyProtoFromInt64({50}))));
+  EXPECT_THAT(scorable_property_set->GetScorablePropertyProto("sender.income"),
+              Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble(
+                  {21001, 21002, 21003}))));
+  EXPECT_THAT(
+      scorable_property_set->GetScorablePropertyProto("sender.isStarred"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromBoolean({false}))));
+  EXPECT_THAT(
+      scorable_property_set->GetScorablePropertyProto("receiver.age"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromInt64({30, 35}))));
+  EXPECT_THAT(
+      scorable_property_set->GetScorablePropertyProto("receiver.income"),
+      Pointee(EqualsProto(BuildScorablePropertyProtoFromDouble(
+          {10000, 20000, 30000, 10001, 20001, 30001}))));
+  EXPECT_THAT(
+      scorable_property_set->GetScorablePropertyProto("receiver.isStarred"),
+      Pointee(
+          EqualsProto(BuildScorablePropertyProtoFromBoolean({true, false}))));
+}
+
+}  // namespace
+
+}  // namespace lib
+}  // namespace icing
diff --git a/icing/util/tokenized-document_test.cc b/icing/util/tokenized-document_test.cc
index ab7f4b9..2aa92d1 100644
--- a/icing/util/tokenized-document_test.cc
+++ b/icing/util/tokenized-document_test.cc
@@ -23,6 +23,7 @@
 #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/portable/platform.h"
 #include "icing/proto/document.pb.h"
@@ -33,11 +34,12 @@
 #include "icing/schema/section.h"
 #include "icing/testing/common-matchers.h"
 #include "icing/testing/fake-clock.h"
-#include "icing/testing/icu-data-file-helper.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 "unicode/uloc.h"
 
 namespace icing {
@@ -84,41 +86,43 @@
 
 const SectionMetadata kIndexableInteger1SectionMetadata(
     kIndexableInteger1SectionId, TYPE_INT64, TOKENIZER_NONE, TERM_MATCH_UNKNOWN,
-    NUMERIC_MATCH_RANGE, EMBEDDING_INDEXING_UNKNOWN,
+    NUMERIC_MATCH_RANGE, EMBEDDING_INDEXING_UNKNOWN, QUANTIZATION_TYPE_NONE,
     std::string(kIndexableIntegerProperty1));
 
 const SectionMetadata kIndexableInteger2SectionMetadata(
     kIndexableInteger2SectionId, TYPE_INT64, TOKENIZER_NONE, TERM_MATCH_UNKNOWN,
-    NUMERIC_MATCH_RANGE, EMBEDDING_INDEXING_UNKNOWN,
+    NUMERIC_MATCH_RANGE, EMBEDDING_INDEXING_UNKNOWN, QUANTIZATION_TYPE_NONE,
     std::string(kIndexableIntegerProperty2));
 
 const SectionMetadata kIndexableVector1SectionMetadata(
     kIndexableVector1SectionId, TYPE_VECTOR, TOKENIZER_NONE, TERM_MATCH_UNKNOWN,
     NUMERIC_MATCH_UNKNOWN, EMBEDDING_INDEXING_LINEAR_SEARCH,
-    std::string(kIndexableVectorProperty1));
+    QUANTIZATION_TYPE_NONE, std::string(kIndexableVectorProperty1));
 
 const SectionMetadata kIndexableVector2SectionMetadata(
     kIndexableVector2SectionId, TYPE_VECTOR, TOKENIZER_NONE, TERM_MATCH_UNKNOWN,
     NUMERIC_MATCH_UNKNOWN, EMBEDDING_INDEXING_LINEAR_SEARCH,
-    std::string(kIndexableVectorProperty2));
+    QUANTIZATION_TYPE_QUANTIZE_8_BIT, std::string(kIndexableVectorProperty2));
 
 const SectionMetadata kStringExactSectionMetadata(
     kStringExactSectionId, TYPE_STRING, TOKENIZER_PLAIN, TERM_MATCH_EXACT,
-    NUMERIC_MATCH_UNKNOWN, EMBEDDING_INDEXING_UNKNOWN,
+    NUMERIC_MATCH_UNKNOWN, EMBEDDING_INDEXING_UNKNOWN, QUANTIZATION_TYPE_NONE,
     std::string(kStringExactProperty));
 
 const SectionMetadata kStringPrefixSectionMetadata(
     kStringPrefixSectionId, TYPE_STRING, TOKENIZER_PLAIN, TERM_MATCH_PREFIX,
-    NUMERIC_MATCH_UNKNOWN, EMBEDDING_INDEXING_UNKNOWN,
+    NUMERIC_MATCH_UNKNOWN, EMBEDDING_INDEXING_UNKNOWN, QUANTIZATION_TYPE_NONE,
     std::string(kStringPrefixProperty));
 
 const JoinablePropertyMetadata kQualifiedId1JoinablePropertyMetadata(
     kQualifiedId1JoinablePropertyId, TYPE_STRING,
-    JOINABLE_VALUE_TYPE_QUALIFIED_ID, std::string(kQualifiedId1));
+    JOINABLE_VALUE_TYPE_QUALIFIED_ID, DELETE_PROPAGATION_TYPE_PROPAGATE_FROM,
+    std::string(kQualifiedId1));
 
 const JoinablePropertyMetadata kQualifiedId2JoinablePropertyMetadata(
     kQualifiedId2JoinablePropertyId, TYPE_STRING,
-    JOINABLE_VALUE_TYPE_QUALIFIED_ID, std::string(kQualifiedId2));
+    JOINABLE_VALUE_TYPE_QUALIFIED_ID, DELETE_PROPAGATION_TYPE_NONE,
+    std::string(kQualifiedId2));
 
 // Other non-indexable/joinable properties.
 constexpr std::string_view kUnindexedStringProperty = "unindexedString";
@@ -128,6 +132,7 @@
 class TokenizedDocumentTest : public ::testing::Test {
  protected:
   void SetUp() override {
+    feature_flags_ = std::make_unique<FeatureFlags>(GetTestFeatureFlags());
     test_dir_ = GetTestTempDir() + "/icing";
     schema_store_dir_ = test_dir_ + "/schema_store";
     filesystem_.CreateDirectoryRecursively(schema_store_dir_.c_str());
@@ -135,7 +140,7 @@
     if (!IsCfStringTokenization() && !IsReverseJniTokenization()) {
       ICING_ASSERT_OK(
           // File generated via icu_data_file rule in //icing/BUILD.
-          icu_data_file_helper::SetUpICUDataFile(
+          icu_data_file_helper::SetUpIcuDataFile(
               GetTestFilePath("icing/icu.dat")));
     }
 
@@ -145,8 +150,8 @@
         language_segmenter_factory::Create(std::move(options)));
 
     ICING_ASSERT_OK_AND_ASSIGN(
-        schema_store_,
-        SchemaStore::Create(&filesystem_, schema_store_dir_, &fake_clock_));
+        schema_store_, SchemaStore::Create(&filesystem_, schema_store_dir_,
+                                           &fake_clock_, feature_flags_.get()));
 
     SchemaProto schema =
         SchemaBuilder()
@@ -181,7 +186,8 @@
                     .AddProperty(
                         PropertyConfigBuilder()
                             .SetName(kIndexableVectorProperty2)
-                            .SetDataTypeVector(EMBEDDING_INDEXING_LINEAR_SEARCH)
+                            .SetDataTypeVector(EMBEDDING_INDEXING_LINEAR_SEARCH,
+                                               QUANTIZATION_TYPE_QUANTIZE_8_BIT)
                             .SetCardinality(CARDINALITY_OPTIONAL))
                     .AddProperty(PropertyConfigBuilder()
                                      .SetName(kStringExactProperty)
@@ -196,12 +202,14 @@
                     .AddProperty(PropertyConfigBuilder()
                                      .SetName(kQualifiedId1)
                                      .SetDataTypeJoinableString(
-                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                         DELETE_PROPAGATION_TYPE_PROPAGATE_FROM)
                                      .SetCardinality(CARDINALITY_OPTIONAL))
                     .AddProperty(PropertyConfigBuilder()
                                      .SetName(kQualifiedId2)
                                      .SetDataTypeJoinableString(
-                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID)
+                                         JOINABLE_VALUE_TYPE_QUALIFIED_ID,
+                                         DELETE_PROPAGATION_TYPE_NONE)
                                      .SetCardinality(CARDINALITY_OPTIONAL)))
             .Build();
     ICING_ASSERT_OK(schema_store_->SetSchema(
@@ -223,6 +231,7 @@
     ASSERT_TRUE(filesystem_.DeleteDirectoryRecursively(test_dir_.c_str()));
   }
 
+  std::unique_ptr<FeatureFlags> feature_flags_;
   Filesystem filesystem_;
   FakeClock fake_clock_;
   std::string test_dir_;
diff --git a/java/src/com/google/android/icing/IcingSearchEngine.java b/java/src/com/google/android/icing/IcingSearchEngine.java
index c8d4516..0dddb2e 100644
--- a/java/src/com/google/android/icing/IcingSearchEngine.java
+++ b/java/src/com/google/android/icing/IcingSearchEngine.java
@@ -109,6 +109,13 @@
 
   @NonNull
   @Override
+  public GetSchemaResultProto getSchemaForDatabase(@NonNull String database) {
+    return IcingSearchEngineUtils.byteArrayToGetSchemaResultProto(
+        icingSearchEngineImpl.getSchemaForDatabase(database));
+  }
+
+  @NonNull
+  @Override
   public GetSchemaTypeResultProto getSchemaType(@NonNull String schemaType) {
     return IcingSearchEngineUtils.byteArrayToGetSchemaTypeResultProto(
         icingSearchEngineImpl.getSchemaType(schemaType));
@@ -161,7 +168,6 @@
         icingSearchEngineImpl.getNextPage(nextPageToken));
   }
 
-  @NonNull
   @Override
   public void invalidateNextPageToken(long nextPageToken) {
     icingSearchEngineImpl.invalidateNextPageToken(nextPageToken);
@@ -176,6 +182,13 @@
 
   @NonNull
   @Override
+  public BlobProto removeBlob(PropertyProto.BlobHandleProto blobHandle) {
+    return IcingSearchEngineUtils.byteArrayToBlobProto(
+        icingSearchEngineImpl.removeBlob(blobHandle.toByteArray()));
+  }
+
+  @NonNull
+  @Override
   public BlobProto openReadBlob(PropertyProto.BlobHandleProto blobHandle) {
     return IcingSearchEngineUtils.byteArrayToBlobProto(
         icingSearchEngineImpl.openReadBlob(blobHandle.toByteArray()));
diff --git a/java/src/com/google/android/icing/IcingSearchEngineImpl.java b/java/src/com/google/android/icing/IcingSearchEngineImpl.java
index 2a76d92..c06f547 100644
--- a/java/src/com/google/android/icing/IcingSearchEngineImpl.java
+++ b/java/src/com/google/android/icing/IcingSearchEngineImpl.java
@@ -95,6 +95,12 @@
   }
 
   @Nullable
+  public byte[] getSchemaForDatabase(@NonNull String database) {
+    throwIfClosed();
+    return nativeGetSchemaForDatabase(this, database);
+  }
+
+  @Nullable
   public byte[] getSchemaType(@NonNull String schemaType) {
     throwIfClosed();
     return nativeGetSchemaType(this, schemaType);
@@ -150,7 +156,6 @@
     return nativeGetNextPage(this, nextPageToken, System.currentTimeMillis());
   }
 
-  @NonNull
   public void invalidateNextPageToken(long nextPageToken) {
     throwIfClosed();
     nativeInvalidateNextPageToken(this, nextPageToken);
@@ -163,6 +168,12 @@
   }
 
   @NonNull
+  public byte[] removeBlob(@NonNull byte[] blobHandleBytes) {
+    throwIfClosed();
+    return nativeRemoveBlob(this, blobHandleBytes);
+  }
+
+  @NonNull
   public byte[] openReadBlob(@NonNull byte[] blobHandleBytes) {
     throwIfClosed();
     return nativeOpenReadBlob(this, blobHandleBytes);
@@ -281,6 +292,9 @@
 
   private static native byte[] nativeGetSchema(IcingSearchEngineImpl instance);
 
+  private static native byte[] nativeGetSchemaForDatabase(
+      IcingSearchEngineImpl instance, String database);
+
   private static native byte[] nativeGetSchemaType(
       IcingSearchEngineImpl instance, String schemaType);
 
@@ -310,6 +324,9 @@
   private static native byte[] nativeOpenWriteBlob(
       IcingSearchEngineImpl instance, byte[] blobHandleBytes);
 
+  private static native byte[] nativeRemoveBlob(
+      IcingSearchEngineImpl instance, byte[] blobHandleBytes);
+
   private static native byte[] nativeOpenReadBlob(
       IcingSearchEngineImpl instance, byte[] blobHandleBytes);
 
diff --git a/java/src/com/google/android/icing/IcingSearchEngineInterface.java b/java/src/com/google/android/icing/IcingSearchEngineInterface.java
index 65b8085..dcd5c3e 100644
--- a/java/src/com/google/android/icing/IcingSearchEngineInterface.java
+++ b/java/src/com/google/android/icing/IcingSearchEngineInterface.java
@@ -58,6 +58,14 @@
   GetSchemaResultProto getSchema();
 
   /**
+   * Gets the schema for the specified database for the icing instance.
+   *
+   * @param database an icing schema database name. The retrieved SchemaProto will only contain
+   *     types that belong to the requested database.
+   */
+  GetSchemaResultProto getSchemaForDatabase(String database);
+
+  /**
    * Gets the schema for the icing instance.
    *
    * @param schemaType type of the schema.
@@ -100,6 +108,9 @@
   /** Gets a file descriptor to write blob data. */
   BlobProto openWriteBlob(PropertyProto.BlobHandleProto blobHandle);
 
+  /** Removes a pending blob. */
+  BlobProto removeBlob(PropertyProto.BlobHandleProto blobHandle);
+
   /** Gets a file descriptor to read blob data. */
   BlobProto openReadBlob(PropertyProto.BlobHandleProto blobHandle);
 
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 e7b114f..fb17f44 100644
--- a/java/tests/instrumentation/src/com/google/android/icing/IcingSearchEngineTest.java
+++ b/java/tests/instrumentation/src/com/google/android/icing/IcingSearchEngineTest.java
@@ -90,6 +90,7 @@
 public final class IcingSearchEngineTest {
   @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
 
+  private static final String SCHEMA_DATABASE_DELIMITER = "/";
   private static final String EMAIL_TYPE = "Email";
 
   private File tempDir;
@@ -120,6 +121,31 @@
         .build();
   }
 
+  private static SchemaTypeConfigProto createEmailTypeConfigWithDatabase(String database) {
+    return SchemaTypeConfigProto.newBuilder()
+        .setSchemaType(database + SCHEMA_DATABASE_DELIMITER + EMAIL_TYPE)
+        .setDatabase(database)
+        .addProperties(
+            PropertyConfigProto.newBuilder()
+                .setPropertyName("subject")
+                .setDataType(PropertyConfigProto.DataType.Code.STRING)
+                .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+                .setStringIndexingConfig(
+                    StringIndexingConfig.newBuilder()
+                        .setTokenizerType(TokenizerType.Code.PLAIN)
+                        .setTermMatchType(TermMatchType.Code.PREFIX)))
+        .addProperties(
+            PropertyConfigProto.newBuilder()
+                .setPropertyName("body")
+                .setDataType(PropertyConfigProto.DataType.Code.STRING)
+                .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL)
+                .setStringIndexingConfig(
+                    StringIndexingConfig.newBuilder()
+                        .setTokenizerType(TokenizerType.Code.PLAIN)
+                        .setTermMatchType(TermMatchType.Code.PREFIX)))
+        .build();
+  }
+
   private static DocumentProto createEmailDocument(String namespace, String uri) {
     return DocumentProto.newBuilder()
         .setNamespace(namespace)
@@ -164,6 +190,72 @@
   }
 
   @Test
+  public void testSetAndGetSchema() throws Exception {
+    assertStatusOk(icingSearchEngine.initialize().getStatus());
+
+    SchemaTypeConfigProto emailTypeConfig = createEmailTypeConfig();
+    SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build();
+    SetSchemaResultProto setSchemaResultProto =
+        icingSearchEngine.setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false);
+    assertStatusOk(setSchemaResultProto.getStatus());
+
+    GetSchemaResultProto getSchemaResultProto = icingSearchEngine.getSchema();
+    assertStatusOk(getSchemaResultProto.getStatus());
+    assertThat(getSchemaResultProto.getSchema()).isEqualTo(schema);
+
+    GetSchemaTypeResultProto getSchemaTypeResultProto =
+        icingSearchEngine.getSchemaType(emailTypeConfig.getSchemaType());
+    assertStatusOk(getSchemaTypeResultProto.getStatus());
+    assertThat(getSchemaTypeResultProto.getSchemaTypeConfig()).isEqualTo(emailTypeConfig);
+  }
+
+  // TODO(b/337913932) re-enable this test after we preregister this API in jni
+  @Ignore
+  @Test
+  public void setAndGetSchemaWithDatabase_ok() throws Exception {
+    IcingSearchEngineOptions options =
+        IcingSearchEngineOptions.newBuilder()
+            .setBaseDir(tempDir.getCanonicalPath())
+            .setEnableSchemaDatabase(true)
+            .build();
+    IcingSearchEngine icingSearchEngine = new IcingSearchEngine(options);
+    assertStatusOk(icingSearchEngine.initialize().getStatus());
+
+    String db1 = "db1";
+    String db2 = "db2";
+    SchemaProto db1Schema =
+        SchemaProto.newBuilder().addTypes(createEmailTypeConfigWithDatabase(db1)).build();
+    SchemaProto db2Schema =
+        SchemaProto.newBuilder().addTypes(createEmailTypeConfigWithDatabase(db2)).build();
+
+    SetSchemaResultProto setSchemaResultProto =
+        icingSearchEngine.setSchema(db1Schema, /* ignoreErrorsAndDeleteDocuments= */ false);
+    assertStatusOk(setSchemaResultProto.getStatus());
+    setSchemaResultProto =
+        icingSearchEngine.setSchema(db2Schema, /* ignoreErrorsAndDeleteDocuments= */ false);
+    assertStatusOk(setSchemaResultProto.getStatus());
+
+    // Get schema for individual databases.
+    GetSchemaResultProto getSchemaResultProto = icingSearchEngine.getSchemaForDatabase(db1);
+    assertStatusOk(getSchemaResultProto.getStatus());
+    assertThat(getSchemaResultProto.getSchema()).isEqualTo(db1Schema);
+
+    getSchemaResultProto = icingSearchEngine.getSchemaForDatabase(db2);
+    assertStatusOk(getSchemaResultProto.getStatus());
+    assertThat(getSchemaResultProto.getSchema()).isEqualTo(db2Schema);
+
+    // The getSchema() API should still return the full schema.
+    SchemaProto fullSchema =
+        SchemaProto.newBuilder()
+            .addTypes(createEmailTypeConfigWithDatabase(db1))
+            .addTypes(createEmailTypeConfigWithDatabase(db2))
+            .build();
+    getSchemaResultProto = icingSearchEngine.getSchema();
+    assertStatusOk(getSchemaResultProto.getStatus());
+    assertThat(getSchemaResultProto.getSchema()).isEqualTo(fullSchema);
+  }
+
+  @Test
   public void testPutAndGetDocuments() throws Exception {
     assertStatusOk(icingSearchEngine.initialize().getStatus());
 
@@ -171,7 +263,7 @@
     SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build();
     assertThat(
             icingSearchEngine
-                .setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false)
+                .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
                 .getStatus()
                 .getCode())
         .isEqualTo(StatusProto.Code.OK);
@@ -194,7 +286,7 @@
     SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build();
     assertThat(
             icingSearchEngine
-                .setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false)
+                .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
                 .getStatus()
                 .getCode())
         .isEqualTo(StatusProto.Code.OK);
@@ -237,7 +329,7 @@
     SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build();
     assertThat(
             icingSearchEngine
-                .setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false)
+                .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
                 .getStatus()
                 .getCode())
         .isEqualTo(StatusProto.Code.OK);
@@ -313,8 +405,8 @@
     byte[] digest = calculateDigest(data);
     PropertyProto.BlobHandleProto blobHandle =
         PropertyProto.BlobHandleProto.newBuilder()
-            .setLabel("label")
             .setDigest(ByteString.copyFrom(digest))
+            .setNamespace("namespace")
             .build();
 
     // 2 Act: write the blob and read it back.
@@ -351,6 +443,52 @@
     assertThat(output).isEqualTo(data);
   }
 
+  @Ignore // b/350530146
+  @Test
+  public void removeBlob() 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); // 10 Bytes
+    byte[] digest = calculateDigest(data);
+    PropertyProto.BlobHandleProto blobHandle =
+        PropertyProto.BlobHandleProto.newBuilder()
+            .setNamespace("ns")
+            .setDigest(ByteString.copyFrom(digest))
+            .build();
+
+    // 2 Act: write the blob and read it back.
+    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);
+    }
+
+    // Remove the blob.
+    BlobProto removeBlobProto = icing.removeBlob(blobHandle);
+    assertStatusOk(removeBlobProto.getStatus());
+
+    // Commit will not found.
+    BlobProto commitBlobProto = icing.commitBlob(blobHandle);
+    assertThat(commitBlobProto.getStatus().getCode()).isEqualTo(StatusProto.Code.NOT_FOUND);
+  }
+
   @Test
   public void testDelete() throws Exception {
     assertStatusOk(icingSearchEngine.initialize().getStatus());
@@ -359,7 +497,7 @@
     SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build();
     assertThat(
             icingSearchEngine
-                .setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false)
+                .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
                 .getStatus()
                 .getCode())
         .isEqualTo(StatusProto.Code.OK);
@@ -383,7 +521,7 @@
     SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build();
     assertThat(
             icingSearchEngine
-                .setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false)
+                .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
                 .getStatus()
                 .getCode())
         .isEqualTo(StatusProto.Code.OK);
@@ -408,7 +546,7 @@
     SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build();
     assertThat(
             icingSearchEngine
-                .setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false)
+                .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
                 .getStatus()
                 .getCode())
         .isEqualTo(StatusProto.Code.OK);
@@ -433,7 +571,7 @@
     SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build();
     assertThat(
             icingSearchEngine
-                .setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false)
+                .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
                 .getStatus()
                 .getCode())
         .isEqualTo(StatusProto.Code.OK);
@@ -488,7 +626,7 @@
     SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build();
     assertThat(
             icingSearchEngine
-                .setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false)
+                .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
                 .getStatus()
                 .getCode())
         .isEqualTo(StatusProto.Code.OK);
@@ -513,7 +651,7 @@
             .build();
 
     DeleteByQueryResultProto deleteResultProto =
-        icingSearchEngine.deleteByQuery(searchSpec, /*returnDeletedDocumentInfo=*/ true);
+        icingSearchEngine.deleteByQuery(searchSpec, /* returnDeletedDocumentInfo= */ true);
     assertStatusOk(deleteResultProto.getStatus());
     DeleteByQueryResultProto.DocumentGroupInfo info =
         DeleteByQueryResultProto.DocumentGroupInfo.newBuilder()
@@ -574,7 +712,7 @@
     SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build();
     assertThat(
             icingSearchEngine
-                .setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false)
+                .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
                 .getStatus()
                 .getCode())
         .isEqualTo(StatusProto.Code.OK);
@@ -603,7 +741,7 @@
     SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build();
     assertThat(
             icingSearchEngine
-                .setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false)
+                .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
                 .getStatus()
                 .getCode())
         .isEqualTo(StatusProto.Code.OK);
@@ -633,7 +771,7 @@
     SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build();
     assertThat(
             icingSearchEngine
-                .setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false)
+                .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
                 .getStatus()
                 .getCode())
         .isEqualTo(StatusProto.Code.OK);
@@ -660,7 +798,9 @@
 
     SchemaProto schema = SchemaProto.newBuilder().addTypes(createEmailTypeConfig()).build();
     assertStatusOk(
-        icingSearchEngine.setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false).getStatus());
+        icingSearchEngine
+            .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
+            .getStatus());
 
     // String:     "天是蓝的"
     //              ^ ^^ ^
@@ -728,7 +868,9 @@
 
     SchemaProto schema = SchemaProto.newBuilder().addTypes(createEmailTypeConfig()).build();
     assertStatusOk(
-        icingSearchEngine.setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false).getStatus());
+        icingSearchEngine
+            .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
+            .getStatus());
 
     // String:    "𐀀𐀁 𐀂𐀃 𐀄"
     //             ^  ^  ^
@@ -797,7 +939,7 @@
     SchemaProto schema = SchemaProto.newBuilder().addTypes(emailTypeConfig).build();
     assertThat(
             icingSearchEngine
-                .setSchema(schema, /*ignoreErrorsAndDeleteDocuments=*/ false)
+                .setSchema(schema, /* ignoreErrorsAndDeleteDocuments= */ false)
                 .getStatus()
                 .getCode())
         .isEqualTo(StatusProto.Code.OK);
diff --git a/proto/icing/proto/blob.proto b/proto/icing/proto/blob.proto
index f2cfe0d..9ff4fc5 100644
--- a/proto/icing/proto/blob.proto
+++ b/proto/icing/proto/blob.proto
@@ -16,6 +16,7 @@
 
 package icing.lib;
 
+import "icing/proto/document.proto";
 import "icing/proto/status.proto";
 
 option java_package = "com.google.android.icing.proto";
@@ -38,3 +39,20 @@
   // The file decriptor of the blob file from Icing.
   optional int32 file_descriptor = 2;
 }
+
+// BlobInfo holds information about a blob. It is used to store in the blob
+// info proto log file.
+// Next tag: 4
+message BlobInfoProto {
+  // The blob handle that represents the blob.
+  optional PropertyProto.BlobHandleProto blob_handle = 1;
+
+  // The creation time of the blob. This is used to determine when to delete
+  // the orphaned blobs.
+  // We are using creation_time_ms to be file name of the blob, so this field
+  // is unique for each blob.
+  optional int64 creation_time_ms = 2;
+
+  // Whether the blob is committed and ready to be read.
+  optional bool is_committed = 3;
+}
diff --git a/proto/icing/proto/document.proto b/proto/icing/proto/document.proto
index 0fee0c0..18ba95f 100644
--- a/proto/icing/proto/document.proto
+++ b/proto/icing/proto/document.proto
@@ -105,12 +105,15 @@
   repeated VectorProto vector_values = 8;
 
   // Holds a blob handle field of the property.
-  // Next tag: 3
+  // Next tag: 4
   message BlobHandleProto {
     // The sha-256 hashed digest for a blob.
     optional bytes digest = 1;
-    // The label of the blob.
-    optional string label = 2;
+
+    // The namespace of the blob.
+    optional string namespace = 3;
+
+    reserved 2;
   }
   repeated BlobHandleProto blob_handle_values = 9;
 }
diff --git a/proto/icing/proto/initialize.proto b/proto/icing/proto/initialize.proto
index 102a59d..35290d0 100644
--- a/proto/icing/proto/initialize.proto
+++ b/proto/icing/proto/initialize.proto
@@ -23,7 +23,7 @@
 option java_multiple_files = true;
 option objc_class_prefix = "ICNG";
 
-// Next tag: 7
+// Next tag: 8
 message IcingSearchEngineFeatureInfoProto {
   // REQUIRED: Enum representing an IcingLib feature flagged using
   // IcingSearchEngineOptions
@@ -41,6 +41,38 @@
     // feature, and related metadata hits indexing used for property existence
     // check.
     FEATURE_HAS_PROPERTY_OPERATOR = 1;
+
+    // Feature for flag IcingSearchEngineOptions::enable_embedding_index.
+    //
+    // This feature covers the embedding index.
+    FEATURE_EMBEDDING_INDEX = 2;
+
+    // Feature for flag IcingSearchEngineOptions::enable_scorable_properties.
+    //
+    // This feature covers the scorable properties feature.
+    FEATURE_SCORABLE_PROPERTIES = 3;
+
+    // Feature for flag IcingSearchEngineOptions::enable_embedding_quantization.
+    //
+    // This feature covers whether to enable quantization for embedding vectors.
+    FEATURE_EMBEDDING_QUANTIZATION = 4;
+
+    // Feature for flag IcingSearchEngineOptions::enable_schema_database.
+    //
+    // This feature covers whether to enable the database field in the schema.
+    // Once enabled, SetSchema will only allow setting schema types from a
+    // single database field at a time.
+    FEATURE_SCHEMA_DATABASE = 5;
+
+    // Feature for flag IcingSearchEngineOptions::
+    // enable_qualified_id_join_index_v3_and_delete_propagate_from.
+    //
+    // This feature covers whether to enable the join index v3 and support
+    // delete propagation PROPAGATE_FROM. Once enabled, join index v3 will be
+    // rebuilt to replace v2, and deleteing a document will also delete its
+    // child document(s) which refer to it via a joinable property with delete
+    // propagation type PROPAGATE_FROM.
+    FEATURE_QUALIFIED_ID_JOIN_INDEX_V3_AND_DELETE_PROPAGATE_FROM = 6;
   }
 
   // Whether the feature requires the document store to be rebuilt.
@@ -62,6 +94,10 @@
   // Whether the feature requires the qualified id join index to be rebuilt.
   // The default value is false.
   optional bool needs_qualified_id_join_index_rebuild = 6;
+
+  // Whether the feature requires the embedding index to be rebuilt.
+  // The default value is false.
+  optional bool needs_embedding_index_rebuild = 7;
 }
 
 // Next tag: 4
@@ -73,7 +109,7 @@
   repeated IcingSearchEngineFeatureInfoProto enabled_features = 3;
 }
 
-// Next tag: 18
+// Next tag: 26
 message IcingSearchEngineOptions {
   // Directory to persist files for Icing. Required.
   // If Icing was previously initialized with this directory, it will reload
@@ -174,6 +210,7 @@
   // indexing latency.
   optional int32 lite_index_sort_size = 13 [default = 8192];  // 8 KiB
 
+  // DEPRECATED: qualified id join index v2 is fully rolled out.
   optional bool use_new_qualified_id_join_index = 14;
 
   // Whether to build the metadata hits used for property existence check, which
@@ -189,7 +226,58 @@
   // If set to 0, the blob will never be deleted.
   optional int64 orphan_blob_time_to_live_ms = 17;
 
-  reserved 2;
+  // Whether to enable schema database.
+  //
+  // If set to true, the schema database field will be used to store and
+  // retrieve schemas, and SetSchema will only allow setting schema types from
+  // a single database field at a time.
+  //
+  // Icing will automatically handle migrating the stored SchemaProto and
+  // populate the database field when this flag is flipped from false to true.
+  //
+  // TODO - b/337913932: Remove this flag once the schema database is fully
+  // rolled out.
+  optional bool enable_schema_database = 18;
+
+  // Whether to enable embedding index.
+  // If set to false, the EmbeddingIndex will only be created with a header, and
+  // embedding properties will not be indexed.
+  // TODO(b/326656531): Change the default value to false when the flag is
+  // propagated from AppSearch.
+  optional bool enable_embedding_index = 19 [default = true];
+
+  // Feature flag guarding the new scorable properties feature.
+  // TODO - b/357105837: Remove this flag once the feature is fully rolled out.
+  optional bool enable_scorable_properties = 21;
+
+  // Whether to enable quantization for embedding vectors.
+  // If set to false, all embedding vectors will not be quantized. Otherwise,
+  // quantization will be controlled by the quantization type specified in the
+  // schema.
+  optional bool enable_embedding_quantization = 22;
+
+  // Level of blob_info_file_log store compression in blob store ,
+  // NO_COMPRESSION = 0, BEST_SPEED = 1, BEST_COMPRESSION = 9
+  // Valid values: [0, 9]
+  // Optional.
+  optional int32 blob_store_compression_level = 23 [default = 3];
+
+  // Whether to allow repeated fields to have a joinable value type.
+  optional bool enable_repeated_field_joins = 24;
+
+  // 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;
+
+  reserved 2, 20;
 }
 
 // Result of a call to IcingSearchEngine.Initialize
diff --git a/proto/icing/proto/internal/scorable_property_set.proto b/proto/icing/proto/internal/scorable_property_set.proto
index 13d1101..fd6f573 100644
--- a/proto/icing/proto/internal/scorable_property_set.proto
+++ b/proto/icing/proto/internal/scorable_property_set.proto
@@ -34,38 +34,107 @@
 // proto. This will be stored in the icing document store to allow faster
 // access of scorable properties during the scoring/ranking phase.
 //
-// TODO(b/357105837): Always use the util class(link TBD after cl/660373640 is
-// submitted) to interpret and populate this proto.
+// Use icing::lib::scorable_property_set.h to build/interpret this proto.
 //
 // See go/appsearch-docjoin-dd for the design doc.
 //
 // Next tag: 2
 message ScorablePropertySetProto {
-  // The order of the properties in this proto should match the order of the
-  // scorable properties in the schema proto.
+  // The order of the properties in this proto should match the lexicographical
+  // order of the scorable properties paths in the schema proto.
+  //
+  // All values under a property path will be merged into a list.
   //
   // For example, if you have a schema like:
-  //   SchemaTypeConfigBuilder()
-  //    .setType('email')
-  //    .AddProperty('subject')    // not scorable
-  //    .AddProperty('importance') // scorable
-  //    .AddProperty('title')      // not scorable
-  //    .AddProperty('score')      // scorable
+  //   SchemaBuilder()
+  //     .AddType(
+  //       SchemaTypeConfigBuilder()
+  //         .setType('person')
+  //           .AddProperty('name')                 // not scorable
+  //           .AddProperty('age')                  // scorable
+  //           .AddProperty('address')              // not scorable
+  //           .AddProperty('income'))              // scorable
+  //     .AddType(
+  //       SchemaTypeConfigBuilder()
+  //         .setType('email')
+  //           .AddProperty('subject')              // not scorable
+  //           .AddProperty('importance')           // scorable
+  //           .AddProperty('title')                // not scorable
+  //           .AddProperty('score')                // scorable
+  //           .AddDocumentProperty('receiver'))    // scorable, person type
+  //           .AddDocumentProperty('sender'))      // scorable, person type
   //
   // And an email document like:
-  //   Document email_document =
-  //     DocumentBuilder()
-  //       .AddStringProperty('subject', 'foo')
-  //       .AddDoubleProperty('score', 1.5, 2.5, 3.5)
-  //       .Build();
+  //   Document email_document {
+  //     schema: 'email'
+  //     properties: {
+  //       name: 'subject'
+  //       string_values: 'foo'
+  //     }
+  //     properties: {
+  //       name: 'score'
+  //       double_values: [1.5, 2.5, 3.5]
+  //     }
+  //     properties: {
+  //       name: 'receiver
+  //       document_values: {
+  //         properties: {
+  //           name: 'age'
+  //           int64_values: [30]
+  //         }
+  //         properties: {
+  //           name: 'income'
+  //           double_values: [10000, 20000]
+  //         }
+  //         properties: {
+  //           name: 'isStarred'
+  //           boolean_values: [true]
+  //         }
+  //       }
+  //       document_values: {
+  //         properties: {
+  //           name: 'age'
+  //           int64_values: [40]
+  //         }
+  //         properties: {
+  //           name: 'income'
+  //           double_values: [30000, 40000]
+  //         }
+  //         properties: {
+  //           name: 'isStarred'
+  //           boolean_values: [false]
+  //         }
+  //       }
+  //     }
+  //   }
   //
   // When email_document is put into the icing document store, the following
   // proto will be generated and stored in icing:
   // {
-  //   properties: {} // empty entry for the 'importance' property
+  //   # For the 'importance' property
+  //   properties: {}
+  //   # For the 'receiver.age' property
   //   properties: {
-  //     double_values: [1.5, 2.5, 3.5] // The 'score' property
+  //     int64_values: [30, 40]
   //   }
+  //   # For the 'receiver.income' property
+  //   properties: {
+  //     double_values: [10000, 20000, 30000, 40000]
+  //   }
+  //   # For the 'receiver.isStarred' property
+  //   properties: {
+  //     boolean_values: [true, false]
+  //   }
+  //   # For the 'score' property
+  //   properties: {
+  //     double_values: [1.5, 2.5, 3.5]
+  //   }
+  //   # For the 'sender.age' property
+  //   properties: {}
+  //   # For the 'sender.income' property
+  //   properties: {}
+  //   # For the 'sender.isStarred' property
+  //   properties: {}
   // }
   //
   // During the scoring/ranking phase, the implicit property order can be used
diff --git a/proto/icing/proto/schema.proto b/proto/icing/proto/schema.proto
index e547c4e..2a461bf 100644
--- a/proto/icing/proto/schema.proto
+++ b/proto/icing/proto/schema.proto
@@ -34,10 +34,10 @@
 // TODO(cassiewang) Define a sample proto file that can be used by tests and for
 // documentation.
 //
-// Next tag: 8
+// Next tag: 9
 message SchemaTypeConfigProto {
-  // REQUIRED: Named type that uniquely identifies the structured, logical
-  // schema being defined.
+  // REQUIRED: Named type that identifies the structured, logical schema being
+  // defined.
   //
   // Recommended format: Human readable string that's one of the types defined
   // in http://schema.org. Eg: DigitalDocument, Message, Person, etc.
@@ -50,6 +50,16 @@
   // future retrieval.
   optional string description = 7;
 
+  // OPTIONAL: Identifies a database that the schema type belongs to. This
+  // groups schema types that are related to each other, which is useful for
+  // setting or retrieving a subset of schema types. If unset, the schema type
+  // will be considered as part of the default empty database group.
+  //
+  // NOTE: Only schemas from one database can be set in a single SetSchema call.
+  // Please use multiple SetSchema calls if you want to set schemas across
+  // multiple databases.
+  optional string database = 8;
+
   // List of all properties that are supported by Documents of this type.
   // An Document should never have properties that are not listed here.
   //
@@ -210,11 +220,29 @@
     }
   }
   optional EmbeddingIndexingType.Code embedding_indexing_type = 1;
+
+  // OPTIONAL: Indicates whether the vector contents of this property should be
+  // quantized. Quantization can reduce the size of the embedding search index,
+  // potentially leading to faster embedding search due to lower I/O bandwidth.
+  //
+  // Quantization is usually very reliable and in most cases will have a
+  // negligible impact on recall. Using quantization is strongly recommended.
+  //
+  // The default value is NONE.
+  message QuantizationType {
+    enum Code {
+      // Contents in this property will not be quantized.
+      NONE = 0;
+      // Contents in this property will be quantized to 8 bits.
+      QUANTIZE_8_BIT = 1;
+    }
+  }
+  optional QuantizationType.Code quantization_type = 2;
 }
 
 // Describes how a property can be used to join this document with another
 // document. See JoinSpecProto (in search.proto) for more details.
-// Next tag: 3
+// Next tag: 4
 message JoinableConfig {
   // OPTIONAL: Indicates what joinable type the content value of this property
   // is.
@@ -234,10 +262,25 @@
   }
   optional ValueType.Code value_type = 1;
 
-  // If the parent document a child document is joined to is deleted, delete the
-  // child document as well. This will only apply to children joined through
-  // QUALIFIED_ID, other (future) joinable value types won't use it.
-  optional bool propagate_delete = 2 [default = false];
+  // OPTIONAL: Indicates how to propagate the deletion between the document and
+  // the (referenced) joinable document.
+  //
+  // The default value is NONE.
+  //
+  // If delete propagation is enabled (i.e. not NONE), then value_type must be
+  // QUALIFIED_ID.
+  message DeletePropagationType {
+    enum Code {
+      // No delete propagation.
+      NONE = 0;
+
+      // Propagate delete from the referenced document to the doucument.
+      PROPAGATE_FROM = 1;
+    }
+  }
+  optional DeletePropagationType.Code delete_propagation_type = 3;
+
+  reserved 2;
 }
 
 // Describes the schema of a single property of Documents that belong to a
diff --git a/proto/icing/proto/scoring.proto b/proto/icing/proto/scoring.proto
index bd05a86..5b83d09 100644
--- a/proto/icing/proto/scoring.proto
+++ b/proto/icing/proto/scoring.proto
@@ -25,7 +25,7 @@
 // Encapsulates the configurations on how Icing should score and rank the search
 // results.
 // TODO(b/170347684): Change all timestamps to seconds.
-// Next tag: 6
+// Next tag: 8
 message ScoringSpecProto {
   // OPTIONAL: Indicates how the search results will be ranked.
   message RankingStrategy {
@@ -113,6 +113,43 @@
   // To set this field, the ranking strategy must be set to
   // ADVANCED_SCORING_EXPRESSION.
   repeated string additional_advanced_scoring_expressions = 5;
+
+  // OPTIONAL: Specifies the schema type alias map for advanced scoring
+  // expression.
+  //
+  // The alias map must be provided for all alias schema types used in the
+  // getScorableProperty function of the advanced scoring expression. Otherwise,
+  // an error will be returned.
+  //
+  // For example, Icing clients can pass a SchemaTypeAliasMapProto to icing:
+  //   schema_type_alias_for_advanced_scoring {
+  //     alias_schema_type: "person"
+  //     schema_types: "package1$database1/person"
+  //     schema_types: "package2$database1/person"
+  //   }
+  //   schema_type_alias_for_advanced_scoring {
+  //     alias_schema_type: "email"
+  //     schema_types: "package3$database2/email"
+  //     schema_types: "package4$database2/email"
+  //   }
+  //
+  // When Icing processes the advanced scoring expression such as:
+  // "getScorableProperty('person', 'rfsScore')",
+  // Icing will
+  //   1. first look up documents with schema type "person".
+  //   2. if the SchemaTypeAliasMapProto is provided, Icing will also look up
+  //      documents with schema types from:
+  //         - "package1$database1/person"
+  //         - "package2$database1/person"
+  //
+  // TODO(b/357105837): Consider moving this to a higher level proto.
+  // Currently, ScoringSpec from both parent and child queries need to provide
+  // this proto, thus introducing duplicates. We can consider moving this to a
+  // higher level proto and send it to downstreams.
+  repeated SchemaTypeAliasMapProto schema_type_alias_map_protos = 6;
+
+  // Features enabled for scoring.
+  repeated ScoringFeatureType scoring_feature_types_enabled = 7 [packed = true];
 }
 
 // Next tag: 3
@@ -173,3 +210,21 @@
   // By default, a property is given a raw, pre-normalized weight of 1.0.
   optional double weight = 2;
 }
+
+// Proto that maps an alias schema type to a list of Icing schema types.
+//
+// Next tag: 3
+message SchemaTypeAliasMapProto {
+  // Alias schema type provided by Icing clients.
+  optional string alias_schema_type = 1;
+  // Schema types in Icing.
+  repeated string schema_types = 2;
+}
+
+// Next tag: 2
+enum ScoringFeatureType {
+  SCORING_FEATURE_TYPE_UNKNOWN = 0;
+
+  // Ranking with getScorableProperty(), in advanced scoring expression.
+  SCORABLE_PROPERTY_RANKING = 1;
+}
diff --git a/proto/icing/proto/search.proto b/proto/icing/proto/search.proto
index 6271ecd..45e3736 100644
--- a/proto/icing/proto/search.proto
+++ b/proto/icing/proto/search.proto
@@ -27,7 +27,7 @@
 option objc_class_prefix = "ICNG";
 
 // Client-supplied specifications on what documents to retrieve.
-// Next tag: 14
+// Next tag: 15
 message SearchSpecProto {
   // REQUIRED: The "raw" query string that users may type. For example, "cat"
   // will search for documents with the term cat in it.
@@ -73,6 +73,13 @@
   // schema filters will not be expanded for polymorphism.
   repeated string schema_type_filters = 4;
 
+  // OPTIONAL: Only search for documents under the specified namespaces and
+  // uris. If unset, all documents will be searched.
+  //
+  // All given NamespaceDocumentUriGroup must specify at least one
+  // document_uri. To exclude a namespace, use namespace_filters instead.
+  repeated NamespaceDocumentUriGroup document_uri_filters = 14;
+
   // Timestamp taken just before sending proto across the JNI boundary from java
   // to native side.
   optional int64 java_to_native_start_timestamp_ms = 5;
diff --git a/proto/icing/proto/storage.proto b/proto/icing/proto/storage.proto
index e0323a1..266576d 100644
--- a/proto/icing/proto/storage.proto
+++ b/proto/icing/proto/storage.proto
@@ -52,7 +52,7 @@
   // LINT.ThenChange()
 }
 
-// Next tag: 15
+// Next tag: 16
 message DocumentStorageInfoProto {
   // Total number of alive documents.
   optional int32 num_alive_documents = 1;
@@ -100,6 +100,10 @@
   // is encountered while calculating this field.
   optional int64 namespace_id_mapper_size = 12;
 
+  // Size of the scorable properties cache in bytes. Will be set to -1 if an IO
+  // error is encountered while calculating this field.
+  optional int64 scorable_property_cache_size = 15;
+
   // Number of namespaces seen from the current documents.
   //
   // TODO(cassiewang): This isn't technically needed anymore since clients can
@@ -159,7 +163,19 @@
   optional float min_free_fraction = 8;
 }
 
-// Next tag: 5
+// Next tag: 4
+message NamespaceBlobStorageInfoProto {
+  // The package name of who own the blobs.
+  optional string namespace = 1;
+
+  // Total size of blobs storage of this package in bytes.
+  optional int64 blob_size = 2;
+
+  // Total number of blobs of this package.
+  optional int32 num_blobs = 3;
+}
+
+// Next tag: 6
 message StorageInfoProto {
   // Total size of Icing’s storage in bytes. Will be set to -1 if an IO error is
   // encountered while calculating this field.
@@ -173,6 +189,9 @@
 
   // Storage information of the index.
   optional IndexStorageInfoProto index_storage_info = 4;
+
+  // Storage information of the BlobStore.
+  repeated NamespaceBlobStorageInfoProto namespace_blob_storage_info = 5;
 }
 
 // Next tag: 3
diff --git a/synced_AOSP_CL_number.txt b/synced_AOSP_CL_number.txt
index 5b9c321..e32586a 100644
--- a/synced_AOSP_CL_number.txt
+++ b/synced_AOSP_CL_number.txt
@@ -1 +1 @@
-set(synced_AOSP_CL_number=669068792)
+set(synced_AOSP_CL_number=702081687)