[go: nahoru, domu]

Add property weights to SearchSpec

This change allows clients to promote and demote properties for a schema
type when scoring documents. For instance, if a client wants to
prefer matches in the "subject" property over the "body" property of
an "Email" schema type, they can assign "subject" a higher weight
than "body".

Test: ./gradlew appsearch:appsearch:connectedCheck
 appsearch:appsearch-platform-storage:connectedCheck
 appsearch:appsearch-local-storage:connectedCheck
Bug: 203700301
Relnote: "This change adds an api to allow clients to specify property
weights to control how matches to query terms in different properties
affects the scored produced by RANKING_STRATEGY_RELEVANCE_SCORING."

Change-Id: I069b9bf88a9bd6b3b4b6fcee4e0a8ec526b71c98
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
index 4a528ad..aee8aef 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
@@ -18,7 +18,7 @@
 
 import static androidx.appsearch.app.SearchSpec.GROUPING_TYPE_PER_PACKAGE;
 import static androidx.appsearch.app.SearchSpec.ORDER_ASCENDING;
-import static androidx.appsearch.app.SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP;
+import static androidx.appsearch.app.SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE;
 import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -32,10 +32,12 @@
 import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
 import androidx.appsearch.testutil.AppSearchTestUtils;
 
+import com.google.android.icing.proto.PropertyWeight;
 import com.google.android.icing.proto.ResultSpecProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.proto.ScoringSpecProto;
 import com.google.android.icing.proto.SearchSpecProto;
+import com.google.android.icing.proto.TypePropertyWeights;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 
@@ -43,6 +45,7 @@
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -91,26 +94,39 @@
     }
 
     @Test
-    public void testToScoringSpecProto()  {
+    public void testToScoringSpecProto() {
+        String prefix = PrefixUtil.createPrefix("package", "database1");
+        String schemaType = "schemaType";
+        String namespace = "namespace";
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setOrder(ORDER_ASCENDING)
-                .setRankingStrategy(RANKING_STRATEGY_CREATION_TIMESTAMP).build();
+                .setRankingStrategy(RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setPropertyWeights(schemaType, ImmutableMap.of("property1", 2.0)).build();
 
         ScoringSpecProto scoringSpecProto = new SearchSpecToProtoConverter(
-                /*queryExpression=*/"query",
-                searchSpec,
-                /*prefixes=*/ImmutableSet.of(),
-                /*namespaceMap=*/ImmutableMap.of(),
-                /*schemaMap=*/ImmutableMap.of()).toScoringSpecProto();
+                /*queryExpression=*/"",
+                searchSpec, /*prefixes=*/ImmutableSet.of(prefix),
+                /*namespaceMap=*/ImmutableMap.of(prefix, ImmutableSet.of(prefix + namespace)),
+                /*schemaMap=*/ImmutableMap.of(prefix, ImmutableMap.of(prefix + schemaType,
+                SchemaTypeConfigProto.getDefaultInstance()))).toScoringSpecProto();
+        TypePropertyWeights typePropertyWeights = TypePropertyWeights.newBuilder()
+                .setSchemaType(prefix + schemaType)
+                .addPropertyWeights(PropertyWeight.newBuilder()
+                        .setPath("property1")
+                        .setWeight(2.0)
+                        .build())
+                .build();
 
         assertThat(scoringSpecProto.getOrderBy().getNumber())
                 .isEqualTo(ScoringSpecProto.Order.Code.ASC_VALUE);
         assertThat(scoringSpecProto.getRankBy().getNumber())
-                .isEqualTo(ScoringSpecProto.RankingStrategy.Code.CREATION_TIMESTAMP_VALUE);
+                .isEqualTo(ScoringSpecProto.RankingStrategy.Code.RELEVANCE_SCORE.getNumber());
+        assertThat(scoringSpecProto.getTypePropertyWeightsList()).containsExactly(
+                typePropertyWeights);
     }
 
     @Test
-    public void testToResultSpecProto()  {
+    public void testToResultSpecProto() {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setResultCountPerPage(123)
                 .setSnippetCount(234)
@@ -134,7 +150,7 @@
     }
 
     @Test
-    public void testToResultSpecProto_groupByPackage()  {
+    public void testToResultSpecProto_groupByPackage() {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 5)
                 .build();
@@ -223,7 +239,7 @@
     public void testToResultSpecProto_groupByNamespaceAndPackage() throws Exception {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setResultGrouping(GROUPING_TYPE_PER_PACKAGE
-                        | SearchSpec.GROUPING_TYPE_PER_NAMESPACE , 5)
+                        | SearchSpec.GROUPING_TYPE_PER_NAMESPACE, 5)
                 .build();
 
         String prefix1 = PrefixUtil.createPrefix("package1", "database");
@@ -288,9 +304,9 @@
                 /*prefixes=*/ImmutableSet.of(prefix1),
                 /*namespaceMap=*/ImmutableMap.of(
                         prefix1, ImmutableSet.of("package$database1/namespace1",
-                            "package$database1/namespace2"),
+                                "package$database1/namespace2"),
                         prefix2, ImmutableSet.of("package$database2/namespace3",
-                            "package$database2/namespace4")),
+                                "package$database2/namespace4")),
                 /*schemaMap=*/ImmutableMap.of());
 
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
@@ -443,7 +459,6 @@
                                 "package$database1/typeA", schemaTypeConfigProto,
                                 "package$database1/typeB", schemaTypeConfigProto)));
         SearchSpecProto searchSpecProto = converter.toSearchSpecProto();
-
         // If there is no intersection of the schema filters that user want to search over and
         // those filters which are stored in AppSearch, return empty.
         assertThat(searchSpecProto.getSchemaTypeFiltersList()).isEmpty();
@@ -534,4 +549,92 @@
                 /*visibilityChecker=*/null);
         assertThat(nonEmptyConverter.hasNothingToSearch()).isTrue();
     }
+
+    @Test
+    public void testConvertPropertyWeights() {
+        String prefix1 = PrefixUtil.createPrefix("package", "database1");
+        String prefix2 = PrefixUtil.createPrefix("package", "database2");
+        String schemaTypeA = "typeA";
+        String schemaTypeB = "typeB";
+
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setPropertyWeights(schemaTypeA, ImmutableMap.of("property1", 1.0, "property2",
+                        2.0))
+                .setPropertyWeights(schemaTypeB, ImmutableMap.of("nested.property", 0.5))
+                .build();
+
+        Map<String, Set<String>> namespaceMap = ImmutableMap.of(
+                prefix1, ImmutableSet.of(prefix1 + "namespace1"),
+                prefix2, ImmutableSet.of(prefix2 + "namespace1")
+        );
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaTypeMap = ImmutableMap.of(
+                prefix1,
+                ImmutableMap.of(prefix1 + schemaTypeA, SchemaTypeConfigProto.getDefaultInstance(),
+                        prefix1 + schemaTypeB, SchemaTypeConfigProto.getDefaultInstance()),
+                prefix2,
+                ImmutableMap.of(prefix2 + schemaTypeA, SchemaTypeConfigProto.getDefaultInstance())
+        );
+
+        SearchSpecToProtoConverter converter =
+                new SearchSpecToProtoConverter(
+                        /*queryExpression=*/"",
+                        searchSpec, /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+                        namespaceMap,
+                        schemaTypeMap);
+
+        TypePropertyWeights expectedTypePropertyWeight1 =
+                TypePropertyWeights.newBuilder().setSchemaType(prefix1 + schemaTypeA)
+                        .addPropertyWeights(PropertyWeight.newBuilder()
+                                .setPath("property1")
+                                .setWeight(1.0))
+                        .addPropertyWeights(PropertyWeight.newBuilder()
+                                .setPath("property2")
+                                .setWeight(2.0))
+                        .build();
+        TypePropertyWeights expectedTypePropertyWeight2 =
+                TypePropertyWeights.newBuilder().setSchemaType(prefix2 + schemaTypeA)
+                        .addPropertyWeights(PropertyWeight.newBuilder()
+                                .setPath("property1")
+                                .setWeight(1.0))
+                        .addPropertyWeights(PropertyWeight.newBuilder()
+                                .setPath("property2")
+                                .setWeight(2.0))
+                        .build();
+        TypePropertyWeights expectedTypePropertyWeight3 =
+                TypePropertyWeights.newBuilder().setSchemaType(prefix1 + schemaTypeB)
+                        .addPropertyWeights(PropertyWeight.newBuilder()
+                                .setPath("nested.property")
+                                .setWeight(0.5))
+                        .build();
+
+        List<TypePropertyWeights> convertedTypePropertyWeights =
+                converter.toScoringSpecProto().getTypePropertyWeightsList();
+
+        assertThat(convertedTypePropertyWeights).containsExactly(expectedTypePropertyWeight1,
+                expectedTypePropertyWeight2, expectedTypePropertyWeight3);
+    }
+
+    @Test
+    public void testConvertPropertyWeights_whenNoWeightsSet() {
+        SearchSpec searchSpec = new SearchSpec.Builder().build();
+        String prefix1 = PrefixUtil.createPrefix("package", "database1");
+        SchemaTypeConfigProto schemaTypeConfigProto =
+                SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
+
+        SearchSpecToProtoConverter converter =
+                new SearchSpecToProtoConverter(
+                        /*queryExpression=*/"",
+                        searchSpec, /*prefixes=*/ImmutableSet.of(prefix1),
+                        /*namespaceMap=*/ImmutableMap.of(
+                            prefix1,
+                            ImmutableSet.of(prefix1 + "namespace1")),
+                        /*schemaMap=*/ImmutableMap.of(
+                            prefix1,
+                            ImmutableMap.of(prefix1 + "typeA", schemaTypeConfigProto)));
+
+        ScoringSpecProto convertedScoringSpecProto = converter.toScoringSpecProto();
+
+        assertThat(convertedScoringSpecProto.getTypePropertyWeightsList()).isEmpty();
+    }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
index c1c4475..8d907c8 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
@@ -31,27 +31,25 @@
 
     @Override
     public boolean isFeatureSupported(@NonNull String feature) {
-        if (Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH.equals(feature)) {
-            return true;
+        switch (feature) {
+            case Features.ADD_PERMISSIONS_AND_GET_VISIBILITY:
+                // fall through
+            case Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA:
+                // fall through
+            case Features.GLOBAL_SEARCH_SESSION_GET_BY_ID:
+                // fall through
+            case Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK:
+                // fall through
+            case Features.NUMERIC_SEARCH:
+                // fall through
+            case Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH:
+                // fall through
+            case Features.SEARCH_SPEC_PROPERTY_WEIGHTS:
+                // fall through
+            case Features.TOKENIZER_TYPE_RFC822:
+                return true;
+            default:
+                return false;
         }
-        if (Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK.equals(feature)) {
-            return true;
-        }
-        if (Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA.equals(feature)) {
-            return true;
-        }
-        if (Features.GLOBAL_SEARCH_SESSION_GET_BY_ID.equals(feature)) {
-            return true;
-        }
-        if (Features.ADD_PERMISSIONS_AND_GET_VISIBILITY.equals(feature)) {
-            return true;
-        }
-        if (Features.TOKENIZER_TYPE_RFC822.equals(feature)) {
-            return true;
-        }
-        if (Features.NUMERIC_SEARCH.equals(feature)) {
-            return true;
-        }
-        return false;
     }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
index 43c5263..3219ce9 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
@@ -35,12 +35,14 @@
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
+import com.google.android.icing.proto.PropertyWeight;
 import com.google.android.icing.proto.ResultSpecProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.proto.ScoringSpecProto;
 import com.google.android.icing.proto.SearchSpecProto;
 import com.google.android.icing.proto.TermMatchType;
 import com.google.android.icing.proto.TypePropertyMask;
+import com.google.android.icing.proto.TypePropertyWeights;
 
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -236,6 +238,8 @@
         protoBuilder.setOrderBy(orderCodeProto).setRankBy(
                 toProtoRankingStrategy(mSearchSpec.getRankingStrategy()));
 
+        addTypePropertyWeights(mSearchSpec.getPropertyWeights(), protoBuilder);
+
         return protoBuilder.build();
     }
 
@@ -264,7 +268,6 @@
         }
     }
 
-
     /**
      * Adds result groupings for each namespace in each package being queried for.
      *
@@ -403,4 +406,42 @@
                             .addAllNamespaces(namespaces).setMaxResults(maxNumResults));
         }
     }
+
+    /**
+     * Adds {@link TypePropertyWeights} to {@link ScoringSpecProto}.
+     *
+     * <p>{@link TypePropertyWeights} are added to the {@link ScoringSpecProto} with database and
+     * package prefixing added to the schema type.
+     *
+     * @param typePropertyWeightsMap a map from unprefixed schema type to an inner-map of property
+     *                               paths to weight.
+     * @param scoringSpecBuilder     scoring spec to add weights to.
+     */
+    private void addTypePropertyWeights(
+            @NonNull Map<String, Map<String, Double>> typePropertyWeightsMap,
+            @NonNull ScoringSpecProto.Builder scoringSpecBuilder) {
+        Preconditions.checkNotNull(scoringSpecBuilder);
+        Preconditions.checkNotNull(typePropertyWeightsMap);
+
+        for (Map.Entry<String, Map<String, Double>> typePropertyWeight :
+                typePropertyWeightsMap.entrySet()) {
+            for (String prefix : mPrefixes) {
+                String prefixedSchemaType = prefix + typePropertyWeight.getKey();
+                if (mTargetPrefixedSchemaFilters.contains(prefixedSchemaType)) {
+                    TypePropertyWeights.Builder typePropertyWeightsBuilder =
+                            TypePropertyWeights.newBuilder().setSchemaType(prefixedSchemaType);
+
+                    for (Map.Entry<String, Double> propertyWeight :
+                            typePropertyWeight.getValue().entrySet()) {
+                        typePropertyWeightsBuilder.addPropertyWeights(
+                                PropertyWeight.newBuilder().setPath(
+                                        propertyWeight.getKey()).setWeight(
+                                        propertyWeight.getValue()));
+                    }
+
+                    scoringSpecBuilder.addTypePropertyWeights(typePropertyWeightsBuilder);
+                }
+            }
+        }
+    }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
index 91acf0b..6fcdb6b 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
@@ -29,31 +29,35 @@
     // TODO(b/201316758): Remove once BuildCompat.isAtLeastT is removed
     @BuildCompat.PrereleaseSdkCheck
     public boolean isFeatureSupported(@NonNull String feature) {
-        if (Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH.equals(feature)) {
-            return BuildCompat.isAtLeastT();
+        switch (feature) {
+            // Android T Features
+            case Features.ADD_PERMISSIONS_AND_GET_VISIBILITY:
+                // fall through
+            case Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA:
+                // fall through
+            case Features.GLOBAL_SEARCH_SESSION_GET_BY_ID:
+                // fall through
+            case Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK:
+                // fall through
+            case Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH:
+                // fall through
+                return BuildCompat.isAtLeastT();
+
+            // Android U Features
+            case Features.SEARCH_SPEC_PROPERTY_WEIGHTS:
+                // TODO(b/203700301) : Update to reflect support in Android U+ once this feature is
+                // synced over into service-appsearch.
+                // fall through
+            case Features.TOKENIZER_TYPE_RFC822:
+                // TODO(b/259294369) : Update to reflect support in Android U+ once this feature is
+                // synced over into service-appsearch.
+                // fall through
+            case Features.NUMERIC_SEARCH:
+                // TODO(b/259744228) : Update to reflect support in Android U+ once this feature is
+                // synced over into service-appsearch.
+                return false;
+            default:
+                return false;
         }
-        if (Features.GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK.equals(feature)) {
-            return BuildCompat.isAtLeastT();
-        }
-        if (Features.GLOBAL_SEARCH_SESSION_GET_SCHEMA.equals(feature)) {
-            return BuildCompat.isAtLeastT();
-        }
-        if (Features.GLOBAL_SEARCH_SESSION_GET_BY_ID.equals(feature)) {
-            return BuildCompat.isAtLeastT();
-        }
-        if (Features.ADD_PERMISSIONS_AND_GET_VISIBILITY.equals(feature)) {
-            return BuildCompat.isAtLeastT();
-        }
-        // TODO(b/259294369): Update to reflect support in Android U+ once this feature is synced
-        // over into service-appsearch
-        if (Features.TOKENIZER_TYPE_RFC822.equals(feature)) {
-            return false;
-        }
-        // TODO(b/259744228): Update to reflect support in Android U+ once this feature is synced
-        // over into service-appsearch
-        if (Features.NUMERIC_SEARCH.equals(feature)) {
-            return false;
-        }
-        return false;
     }
 }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
index c6d6d7e..9bdb118 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/converter/SearchSpecToPlatformConverter.java
@@ -47,8 +47,10 @@
     public static android.app.appsearch.SearchSpec toPlatformSearchSpec(
             @NonNull SearchSpec jetpackSearchSpec) {
         Preconditions.checkNotNull(jetpackSearchSpec);
+
         android.app.appsearch.SearchSpec.Builder platformBuilder =
                 new android.app.appsearch.SearchSpec.Builder();
+
         platformBuilder
                 .setTermMatch(jetpackSearchSpec.getTermMatch())
                 .addFilterSchemas(jetpackSearchSpec.getFilterSchemas())
@@ -69,6 +71,14 @@
                 jetpackSearchSpec.getProjections().entrySet()) {
             platformBuilder.addProjection(projection.getKey(), projection.getValue());
         }
+
+        // TODO(b/203700301) : Update to reflect support in Android U+ once this
+        // feature is synced over into service-appsearch.
+        if (!jetpackSearchSpec.getPropertyWeights().isEmpty()) {
+            throw new UnsupportedOperationException(
+                    "Property weights are not supported with this backend/Android API level "
+                            + "combination.");
+        }
         return platformBuilder.build();
     }
 }
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index ea863f8..4486da3 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -211,6 +211,7 @@
     field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
     field public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH";
     field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+    field public static final String SEARCH_SPEC_PROPERTY_WEIGHTS = "SEARCH_SPEC_PROPERTY_WEIGHTS";
     field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
   }
 
@@ -443,6 +444,8 @@
     method public int getOrder();
     method public java.util.Map<java.lang.String!,java.util.List<androidx.appsearch.app.PropertyPath!>!> getProjectionPaths();
     method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
+    method public java.util.Map<java.lang.String!,java.util.Map<androidx.appsearch.app.PropertyPath!,java.lang.Double!>!> getPropertyWeightPaths();
+    method public java.util.Map<java.lang.String!,java.util.Map<java.lang.String!,java.lang.Double!>!> getPropertyWeights();
     method public int getRankingStrategy();
     method public int getResultCountPerPage();
     method public int getResultGroupingLimit();
@@ -484,6 +487,10 @@
     method public androidx.appsearch.app.SearchSpec build();
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_PROPERTY_WEIGHTS) public androidx.appsearch.app.SearchSpec.Builder setPropertyWeightPaths(String, java.util.Map<androidx.appsearch.app.PropertyPath!,java.lang.Double!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_PROPERTY_WEIGHTS) public androidx.appsearch.app.SearchSpec.Builder setPropertyWeightPathsForDocumentClass(Class<?>, java.util.Map<androidx.appsearch.app.PropertyPath!,java.lang.Double!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_PROPERTY_WEIGHTS) public androidx.appsearch.app.SearchSpec.Builder setPropertyWeights(String, java.util.Map<java.lang.String!,java.lang.Double!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_PROPERTY_WEIGHTS) public androidx.appsearch.app.SearchSpec.Builder setPropertyWeightsForDocumentClass(Class<?>, java.util.Map<java.lang.String!,java.lang.Double!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(int);
     method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);
diff --git a/appsearch/appsearch/api/public_plus_experimental_current.txt b/appsearch/appsearch/api/public_plus_experimental_current.txt
index ea863f8..4486da3 100644
--- a/appsearch/appsearch/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch/api/public_plus_experimental_current.txt
@@ -211,6 +211,7 @@
     field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
     field public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH";
     field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+    field public static final String SEARCH_SPEC_PROPERTY_WEIGHTS = "SEARCH_SPEC_PROPERTY_WEIGHTS";
     field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
   }
 
@@ -443,6 +444,8 @@
     method public int getOrder();
     method public java.util.Map<java.lang.String!,java.util.List<androidx.appsearch.app.PropertyPath!>!> getProjectionPaths();
     method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
+    method public java.util.Map<java.lang.String!,java.util.Map<androidx.appsearch.app.PropertyPath!,java.lang.Double!>!> getPropertyWeightPaths();
+    method public java.util.Map<java.lang.String!,java.util.Map<java.lang.String!,java.lang.Double!>!> getPropertyWeights();
     method public int getRankingStrategy();
     method public int getResultCountPerPage();
     method public int getResultGroupingLimit();
@@ -484,6 +487,10 @@
     method public androidx.appsearch.app.SearchSpec build();
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_PROPERTY_WEIGHTS) public androidx.appsearch.app.SearchSpec.Builder setPropertyWeightPaths(String, java.util.Map<androidx.appsearch.app.PropertyPath!,java.lang.Double!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_PROPERTY_WEIGHTS) public androidx.appsearch.app.SearchSpec.Builder setPropertyWeightPathsForDocumentClass(Class<?>, java.util.Map<androidx.appsearch.app.PropertyPath!,java.lang.Double!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_PROPERTY_WEIGHTS) public androidx.appsearch.app.SearchSpec.Builder setPropertyWeights(String, java.util.Map<java.lang.String!,java.lang.Double!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_PROPERTY_WEIGHTS) public androidx.appsearch.app.SearchSpec.Builder setPropertyWeightsForDocumentClass(Class<?>, java.util.Map<java.lang.String!,java.lang.Double!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(int);
     method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index ea863f8..4486da3 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -211,6 +211,7 @@
     field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
     field public static final String NUMERIC_SEARCH = "NUMERIC_SEARCH";
     field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+    field public static final String SEARCH_SPEC_PROPERTY_WEIGHTS = "SEARCH_SPEC_PROPERTY_WEIGHTS";
     field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
   }
 
@@ -443,6 +444,8 @@
     method public int getOrder();
     method public java.util.Map<java.lang.String!,java.util.List<androidx.appsearch.app.PropertyPath!>!> getProjectionPaths();
     method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
+    method public java.util.Map<java.lang.String!,java.util.Map<androidx.appsearch.app.PropertyPath!,java.lang.Double!>!> getPropertyWeightPaths();
+    method public java.util.Map<java.lang.String!,java.util.Map<java.lang.String!,java.lang.Double!>!> getPropertyWeights();
     method public int getRankingStrategy();
     method public int getResultCountPerPage();
     method public int getResultGroupingLimit();
@@ -484,6 +487,10 @@
     method public androidx.appsearch.app.SearchSpec build();
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_PROPERTY_WEIGHTS) public androidx.appsearch.app.SearchSpec.Builder setPropertyWeightPaths(String, java.util.Map<androidx.appsearch.app.PropertyPath!,java.lang.Double!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_PROPERTY_WEIGHTS) public androidx.appsearch.app.SearchSpec.Builder setPropertyWeightPathsForDocumentClass(Class<?>, java.util.Map<androidx.appsearch.app.PropertyPath!,java.lang.Double!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_PROPERTY_WEIGHTS) public androidx.appsearch.app.SearchSpec.Builder setPropertyWeights(String, java.util.Map<java.lang.String!,java.lang.Double!>);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_PROPERTY_WEIGHTS) public androidx.appsearch.app.SearchSpec.Builder setPropertyWeightsForDocumentClass(Class<?>, java.util.Map<java.lang.String!,java.lang.Double!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec.Builder setRankingStrategy(int);
     method public androidx.appsearch.app.SearchSpec.Builder setResultCountPerPage(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setResultGrouping(int, int);
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
index befacdd..e6aefc0 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
@@ -60,6 +60,7 @@
 import androidx.test.core.app.ApplicationProvider;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
@@ -3518,4 +3519,252 @@
         // "google.com", ">"]. So "com" will not match any of the tokens produced.
         assertThat(sr.getNextPageAsync().get()).hasSize(0);
     }
+
+    @Test
+    public void testQuery_propertyWeights() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setSubject("foo")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setBody("foo")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Query for "foo". It should match both emails.
+        SearchResults searchResults = mDb1.search("foo", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setOrder(SearchSpec.ORDER_DESCENDING)
+                .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of("subject",
+                        2.0, "body", 0.5))
+                .build());
+        List<SearchResult> results = retrieveAllSearchResults(searchResults);
+
+        // email1 should be ranked higher because "foo" appears in the "subject" property which
+        // has higher weight than the "body" property.
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getRankingSignal()).isGreaterThan(0);
+        assertThat(results.get(0).getRankingSignal()).isGreaterThan(
+                results.get(1).getRankingSignal());
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(email1);
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(email2);
+
+        // Query for "foo" without property weights.
+        SearchSpec searchSpecWithoutWeights = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setOrder(SearchSpec.ORDER_DESCENDING)
+                .build();
+        SearchResults searchResultsWithoutWeights = mDb1.search("foo", searchSpecWithoutWeights);
+        List<SearchResult> resultsWithoutWeights =
+                retrieveAllSearchResults(searchResultsWithoutWeights);
+
+        // email1 should have the same ranking signal as email2 as each contains the term "foo"
+        // once.
+        assertThat(resultsWithoutWeights).hasSize(2);
+        assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isGreaterThan(0);
+        assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isEqualTo(
+                resultsWithoutWeights.get(1).getRankingSignal());
+    }
+
+    @Test
+    public void testQuery_propertyWeightsNestedProperties() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
+
+        // Register a schema with a nested type
+        AppSearchSchema schema =
+                new AppSearchSchema.Builder("TypeA").addProperty(
+                        new AppSearchSchema.DocumentPropertyConfig.Builder("nestedEmail",
+                                AppSearchEmail.SCHEMA_TYPE).setShouldIndexNestedProperties(
+                                true).build()).build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA,
+                schema).build()).get();
+
+        // Index two documents
+        AppSearchEmail nestedEmail1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setSubject("foo")
+                        .build();
+        GenericDocument doc1 =
+                new GenericDocument.Builder<>("namespace", "id1", "TypeA").setPropertyDocument(
+                        "nestedEmail", nestedEmail1).build();
+        AppSearchEmail nestedEmail2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setBody("foo")
+                        .build();
+        GenericDocument doc2 =
+                new GenericDocument.Builder<>("namespace", "id2", "TypeA").setPropertyDocument(
+                        "nestedEmail", nestedEmail2).build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(doc1, doc2).build()));
+
+        // Query for "foo". It should match both emails.
+        SearchResults searchResults = mDb1.search("foo", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setOrder(SearchSpec.ORDER_DESCENDING)
+                .setPropertyWeights("TypeA", ImmutableMap.of(
+                        "nestedEmail.subject",
+                        2.0, "nestedEmail.body", 0.5))
+                .build());
+        List<SearchResult> results = retrieveAllSearchResults(searchResults);
+
+        // email1 should be ranked higher because "foo" appears in the "nestedEmail.subject"
+        // property which has higher weight than the "nestedEmail.body" property.
+        assertThat(results).hasSize(2);
+        assertThat(results.get(0).getRankingSignal()).isGreaterThan(0);
+        assertThat(results.get(0).getRankingSignal()).isGreaterThan(
+                results.get(1).getRankingSignal());
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(doc1);
+        assertThat(results.get(1).getGenericDocument()).isEqualTo(doc2);
+
+        // Query for "foo" without property weights.
+        SearchSpec searchSpecWithoutWeights = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setOrder(SearchSpec.ORDER_DESCENDING)
+                .build();
+        SearchResults searchResultsWithoutWeights = mDb1.search("foo", searchSpecWithoutWeights);
+        List<SearchResult> resultsWithoutWeights =
+                retrieveAllSearchResults(searchResultsWithoutWeights);
+
+        // email1 should have the same ranking signal as email2 as each contains the term "foo"
+        // once.
+        assertThat(resultsWithoutWeights).hasSize(2);
+        assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isGreaterThan(0);
+        assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isEqualTo(
+                resultsWithoutWeights.get(1).getRankingSignal());
+    }
+
+    @Test
+    public void testQuery_propertyWeightsDefaults() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setSubject("foo")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setBody("foo bar")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1, email2).build()));
+
+        // Query for "foo" without assigning property weights for any path.
+        SearchResults searchResults = mDb1.search("foo", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setOrder(SearchSpec.ORDER_DESCENDING)
+                .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of())
+                .build());
+        List<SearchResult> resultsWithoutPropertyWeights = retrieveAllSearchResults(
+                searchResults);
+
+        // Query for "foo" with assigning default property weights.
+        searchResults = mDb1.search("foo", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setOrder(SearchSpec.ORDER_DESCENDING)
+                .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of("subject", 1.0,
+                        "body", 1.0))
+                .build());
+        List<SearchResult> expectedResults = retrieveAllSearchResults(searchResults);
+
+        assertThat(resultsWithoutPropertyWeights).hasSize(2);
+        assertThat(expectedResults).hasSize(2);
+
+        assertThat(resultsWithoutPropertyWeights.get(0).getGenericDocument()).isEqualTo(email1);
+        assertThat(resultsWithoutPropertyWeights.get(1).getGenericDocument()).isEqualTo(email2);
+        assertThat(expectedResults.get(0).getGenericDocument()).isEqualTo(email1);
+        assertThat(expectedResults.get(1).getGenericDocument()).isEqualTo(email2);
+
+        // The ranking signal for results with no property path and weights set should be equal
+        // to the ranking signal for results with explicitly set default weights.
+        assertThat(resultsWithoutPropertyWeights.get(0).getRankingSignal()).isEqualTo(
+                expectedResults.get(0).getRankingSignal());
+        assertThat(resultsWithoutPropertyWeights.get(1).getRankingSignal()).isEqualTo(
+                expectedResults.get(1).getRankingSignal());
+    }
+
+    @Test
+    public void testQuery_propertyWeightsIgnoresInvalidPropertyPaths() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder()
+                        .addSchemas(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index an email
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setSubject("baz")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(email1).build()));
+
+        // Query for "baz" with property weight for "subject", a valid property in the schema type.
+        SearchResults searchResults = mDb1.search("baz", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setOrder(SearchSpec.ORDER_DESCENDING)
+                .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of("subject", 2.0))
+                .build());
+        List<SearchResult> results = retrieveAllSearchResults(searchResults);
+
+        // Query for "baz" with property weights, one for valid property "subject" and one for a
+        // non-existing property "invalid".
+        searchResults = mDb1.search("baz", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setOrder(SearchSpec.ORDER_DESCENDING)
+                .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE, ImmutableMap.of("subject", 2.0,
+                        "invalid", 3.0))
+                .build());
+        List<SearchResult> resultsWithInvalidPath = retrieveAllSearchResults(searchResults);
+
+        assertThat(results).hasSize(1);
+        assertThat(resultsWithInvalidPath).hasSize(1);
+
+        // We expect the ranking signal to be unchanged in the presence of an invalid property
+        // weight.
+        assertThat(results.get(0).getRankingSignal()).isGreaterThan(0);
+        assertThat(resultsWithInvalidPath.get(0).getRankingSignal()).isEqualTo(
+                results.get(0).getRankingSignal());
+
+        assertThat(results.get(0).getGenericDocument()).isEqualTo(email1);
+        assertThat(resultsWithInvalidPath.get(0).getGenericDocument()).isEqualTo(email1);
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
index e619210..866b963 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionLocalCtsTest.java
@@ -87,6 +87,8 @@
                 Features.GLOBAL_SEARCH_SESSION_GET_BY_ID)).isTrue();
         assertThat(db2.getFeatures().isFeatureSupported(
                 Features.ADD_PERMISSIONS_AND_GET_VISIBILITY)).isTrue();
+        assertThat(db2.getFeatures().isFeatureSupported(
+                Features.SEARCH_SPEC_PROPERTY_WEIGHTS)).isTrue();
     }
 
     // TODO(b/194207451) This test can be moved to CtsTestBase if customized logger is
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
index 98fa981..dd71fc5 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/GlobalSearchSessionCtsTestBase.java
@@ -18,6 +18,7 @@
 
 import static androidx.appsearch.testutil.AppSearchTestUtils.checkIsBatchResultSuccess;
 import static androidx.appsearch.testutil.AppSearchTestUtils.convertSearchResultsToDocuments;
+import static androidx.appsearch.testutil.AppSearchTestUtils.retrieveAllSearchResults;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -55,6 +56,7 @@
 import androidx.test.core.app.ApplicationProvider;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.ListenableFuture;
 
@@ -1830,4 +1832,72 @@
                         mContext.getPackageName(), DB_NAME_1, ImmutableSet.of("Type1")));
         assertThat(observer.getDocumentChanges()).isEmpty();
     }
+
+    @Test
+    public void testGlobalQuery_propertyWeights() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.SEARCH_SPEC_PROPERTY_WEIGHTS));
+
+        // Schema registration
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+        mDb2.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(AppSearchEmail.SCHEMA).build()).get();
+
+        // Put two documents in separate databases.
+        AppSearchEmail emailDb1 =
+                new AppSearchEmail.Builder("namespace", "id1")
+                        .setCreationTimestampMillis(1000)
+                        .setSubject("foo")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocuments(emailDb1).build()));
+        AppSearchEmail emailDb2 =
+                new AppSearchEmail.Builder("namespace", "id2")
+                        .setCreationTimestampMillis(1000)
+                        .setBody("foo")
+                        .build();
+        checkIsBatchResultSuccess(mDb2.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(emailDb2).build()));
+
+        // Issue global query for "foo".
+        SearchResults searchResults = mGlobalSearchSession.search("foo", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setOrder(SearchSpec.ORDER_DESCENDING)
+                .setPropertyWeights(AppSearchEmail.SCHEMA_TYPE,
+                        ImmutableMap.of("subject",
+                                2.0, "body", 0.5))
+                .build());
+        List<SearchResult> globalResults = retrieveAllSearchResults(searchResults);
+
+        // We expect to two emails, one from each of the databases.
+        assertThat(globalResults).hasSize(2);
+        assertThat(globalResults.get(0).getGenericDocument()).isEqualTo(emailDb1);
+        assertThat(globalResults.get(1).getGenericDocument()).isEqualTo(emailDb2);
+
+        // We expect that the email added to db1 will have a higher score than the email added to
+        // db2 as the query term "foo" is contained in the "subject" property which has a higher
+        // weight than the "body" property.
+        assertThat(globalResults.get(0).getRankingSignal()).isGreaterThan(0);
+        assertThat(globalResults.get(0).getRankingSignal()).isGreaterThan(
+                globalResults.get(1).getRankingSignal());
+
+        // Query for "foo" without property weights.
+        SearchResults searchResultsWithoutWeights = mGlobalSearchSession.search("foo",
+                new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                        .setOrder(SearchSpec.ORDER_DESCENDING)
+                        .build());
+        List<SearchResult> resultsWithoutWeights =
+                retrieveAllSearchResults(searchResultsWithoutWeights);
+
+        // email1 should have the same ranking signal as email2 as each contains the term "foo"
+        // once.
+        assertThat(resultsWithoutWeights).hasSize(2);
+        assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isGreaterThan(0);
+        assertThat(resultsWithoutWeights.get(0).getRankingSignal()).isEqualTo(
+                resultsWithoutWeights.get(1).getRankingSignal());
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
index 2816781..5f0de5a 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
@@ -21,15 +21,19 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
+
 import androidx.appsearch.annotation.Document;
 import androidx.appsearch.app.PropertyPath;
 import androidx.appsearch.app.SearchSpec;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 
 import org.junit.Test;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -45,6 +49,11 @@
     public void testBuildSearchSpec() {
         List<String> expectedPropertyPaths1 = ImmutableList.of("path1", "path2");
         List<String> expectedPropertyPaths2 = ImmutableList.of("path3", "path4");
+        Map<String, Double> expectedPropertyWeights = ImmutableMap.of("property1", 1.0,
+                "property2", 2.0);
+        Map<PropertyPath, Double> expectedPropertyWeightPaths =
+                ImmutableMap.of(new PropertyPath("property1.nested"), 1.0);
+
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
                 .addFilterNamespaces("namespace1", "namespace2")
@@ -63,6 +72,8 @@
                         | SearchSpec.GROUPING_TYPE_PER_PACKAGE, /*limit=*/ 37)
                 .addProjection("schemaType1", expectedPropertyPaths1)
                 .addProjection("schemaType2", expectedPropertyPaths2)
+                .setPropertyWeights("schemaType1", expectedPropertyWeights)
+                .setPropertyWeightPaths("schemaType2", expectedPropertyWeightPaths)
                 .build();
 
         assertThat(searchSpec.getTermMatch()).isEqualTo(SearchSpec.TERM_MATCH_PREFIX);
@@ -86,6 +97,17 @@
                 .containsExactly("schemaType1", expectedPropertyPaths1, "schemaType2",
                         expectedPropertyPaths2);
         assertThat(searchSpec.getResultGroupingLimit()).isEqualTo(37);
+        assertThat(searchSpec.getPropertyWeights().keySet()).containsExactly("schemaType1",
+                "schemaType2");
+        assertThat(searchSpec.getPropertyWeights().get("schemaType1"))
+                .containsExactly("property1", 1.0, "property2", 2.0);
+        assertThat(searchSpec.getPropertyWeights().get("schemaType2"))
+                .containsExactly("property1.nested", 1.0);
+        assertThat(searchSpec.getPropertyWeightPaths().get("schemaType1"))
+                .containsExactly(new PropertyPath("property1"), 1.0,
+                        new PropertyPath("property2"), 2.0);
+        assertThat(searchSpec.getPropertyWeightPaths().get("schemaType2"))
+                .containsExactly(new PropertyPath("property1.nested"), 1.0);
     }
 
     @Test
@@ -105,6 +127,134 @@
         assertThat(typePropertyPathMap.get("TypeC")).isEmpty();
     }
 
+    @Test
+    public void testGetTypePropertyWeights() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setPropertyWeights("TypeA", ImmutableMap.of("property1", 1.0, "property2", 2.0))
+                .setPropertyWeights("TypeB", ImmutableMap.of("property1", 1.0, "property2"
+                        + ".nested", 2.0))
+                .build();
+
+        Map<String, Map<String, Double>> typePropertyWeightsMap = searchSpec.getPropertyWeights();
+
+        assertThat(typePropertyWeightsMap.keySet())
+                .containsExactly("TypeA", "TypeB");
+        assertThat(typePropertyWeightsMap.get("TypeA")).containsExactly("property1", 1.0,
+                "property2", 2.0);
+        assertThat(typePropertyWeightsMap.get("TypeB")).containsExactly("property1", 1.0,
+                "property2.nested", 2.0);
+    }
+
+    @Test
+    public void testGetTypePropertyWeightPaths() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setPropertyWeightPaths("TypeA",
+                    ImmutableMap.of(new PropertyPath("property1"), 1.0,
+                                    new PropertyPath("property2"), 2.0))
+                .setPropertyWeightPaths("TypeB",
+                    ImmutableMap.of(new PropertyPath("property1"), 1.0,
+                                    new PropertyPath("property2.nested"), 2.0))
+                .build();
+
+        Map<String, Map<PropertyPath, Double>> typePropertyWeightsMap =
+                searchSpec.getPropertyWeightPaths();
+
+        assertThat(typePropertyWeightsMap.keySet())
+                .containsExactly("TypeA", "TypeB");
+        assertThat(typePropertyWeightsMap.get("TypeA"))
+                .containsExactly(new PropertyPath("property1"), 1.0,
+                             new PropertyPath("property2"), 2.0);
+        assertThat(typePropertyWeightsMap.get("TypeB"))
+                .containsExactly(new PropertyPath("property1"), 1.0,
+                             new PropertyPath("property2.nested"), 2.0);
+    }
+
+    @Test
+    public void testSetPropertyWeights_nonPositiveWeight() {
+        SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder();
+        Map<String, Double> negativePropertyWeight = ImmutableMap.of("property", -1.0);
+
+        assertThrows(IllegalArgumentException.class,
+                () -> searchSpecBuilder.setPropertyWeights("TypeA",
+                        negativePropertyWeight));
+
+        Map<String, Double> zeroPropertyWeight = ImmutableMap.of("property", 0.0);
+        assertThrows(IllegalArgumentException.class,
+                () -> searchSpecBuilder.setPropertyWeights("TypeA", zeroPropertyWeight));
+    }
+
+    @Test
+    public void testSetPropertyWeightPaths_nonPositiveWeight() {
+        SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder();
+        Map<PropertyPath, Double> negativePropertyWeight =
+                ImmutableMap.of(new PropertyPath("property"), -1.0);
+
+        assertThrows(IllegalArgumentException.class,
+                () -> searchSpecBuilder.setPropertyWeightPaths("TypeA",
+                        negativePropertyWeight));
+
+        Map<PropertyPath, Double> zeroPropertyWeight =
+                ImmutableMap.of(new PropertyPath("property"), 0.0);
+        assertThrows(IllegalArgumentException.class,
+                () -> searchSpecBuilder.setPropertyWeightPaths("TypeA", zeroPropertyWeight));
+    }
+
+    @Test
+    public void testSetPropertyWeights_queryIndependentRankingStrategy() throws Exception {
+        SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
+                .setPropertyWeights("TypeA", ImmutableMap.of("property1", 1.0, "property2", 2.0));
+
+        assertThrows(IllegalArgumentException.class, () -> searchSpecBuilder.build());
+    }
+
+    @Test
+    public void testSetPropertyWeightPaths_queryIndependentRankingStrategy() throws Exception {
+        SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP)
+                .setPropertyWeightPaths("TypeA",
+                                        ImmutableMap.of(new PropertyPath("property1"), 1.0,
+                                                        new PropertyPath("property2"), 2.0));
+
+        assertThrows(IllegalArgumentException.class, () -> searchSpecBuilder.build());
+    }
+
+    @Test
+    public void testBuild_builtObjectsAreImmutable() throws Exception {
+        SearchSpec.Builder searchSpecBuilder = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setPropertyWeights("TypeA", ImmutableMap.of("property1", 1.0, "property2", 2.0));
+
+        SearchSpec originalSpec = searchSpecBuilder.build();
+
+        // Modify the builder.
+        SearchSpec newSpec =
+                searchSpecBuilder.setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY).setPropertyWeights(
+                        "TypeA", Collections.emptyMap()).build();
+
+
+        // Verify that 1) the changes took effect on the builder and 2) originalSpec was unaffected.
+        assertThat(newSpec.getTermMatch()).isEqualTo(SearchSpec.TERM_MATCH_EXACT_ONLY);
+        assertThat(newSpec.getRankingStrategy()).isEqualTo(
+                SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE);
+        assertThat(newSpec.getPropertyWeights().keySet()).containsExactly("TypeA");
+        assertThat(newSpec.getPropertyWeights().get("TypeA")).isEmpty();
+
+        assertThat(originalSpec.getTermMatch()).isEqualTo(SearchSpec.TERM_MATCH_PREFIX);
+        assertThat(originalSpec.getRankingStrategy()).isEqualTo(
+                SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE);
+        assertThat(originalSpec.getPropertyWeights().keySet()).containsExactly("TypeA");
+        assertThat(originalSpec.getPropertyWeights().get("TypeA").keySet()).containsExactly(
+                "property1", "property2");
+    }
+
 // @exportToFramework:startStrip()
     @Document
     static class King extends Card {
@@ -152,5 +302,57 @@
         assertThat(searchSpec.getProjections().get("King"))
                 .containsExactly("field3", "field4.subfield3");
     }
+
+    @Test
+    public void testTypePropertyWeightsForDocumentClass() throws Exception {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setPropertyWeightsForDocumentClass(
+                    King.class,
+                    ImmutableMap.of("field1", 1.0, "field2.subfield2", 2.0))
+                .build();
+
+        Map<String, Map<String, Double>> typePropertyWeightsMap = searchSpec.getPropertyWeights();
+        assertThat(typePropertyWeightsMap.keySet())
+                .containsExactly("King");
+        assertThat(typePropertyWeightsMap.get("King")).containsExactly("field1", 1.0,
+                "field2.subfield2", 2.0);
+
+        Map<String, Map<PropertyPath, Double>> typePropertyWeightPathsMap =
+                searchSpec.getPropertyWeightPaths();
+        assertThat(typePropertyWeightPathsMap.keySet())
+                .containsExactly("King");
+        assertThat(typePropertyWeightPathsMap.get("King"))
+                .containsExactly(new PropertyPath("field1"), 1.0,
+                             new PropertyPath("field2.subfield2"), 2.0);
+    }
+
+    @Test
+    public void testTypePropertyWeightPathsForDocumentClass() throws Exception {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .setPropertyWeightPathsForDocumentClass(
+                    King.class,
+                    ImmutableMap.of(new PropertyPath("field1"), 1.0,
+                                    new PropertyPath("field2.subfield2"), 2.0))
+                .build();
+
+        Map<String, Map<String, Double>> typePropertyWeightsMap = searchSpec.getPropertyWeights();
+        assertThat(typePropertyWeightsMap.keySet())
+                .containsExactly("King");
+        assertThat(typePropertyWeightsMap.get("King")).containsExactly("field1", 1.0,
+                "field2.subfield2", 2.0);
+
+        Map<String, Map<PropertyPath, Double>> typePropertyWeightPathsMap =
+                searchSpec.getPropertyWeightPaths();
+        assertThat(typePropertyWeightPathsMap.keySet())
+                .containsExactly("King");
+        assertThat(typePropertyWeightPathsMap.get("King"))
+                .containsExactly(new PropertyPath("field1"), 1.0,
+                             new PropertyPath("field2.subfield2"), 2.0);
+    }
+
 // @exportToFramework:endStrip()
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
index 07b5fdb..3440f20 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
@@ -81,6 +81,11 @@
      */
     String NUMERIC_SEARCH = "NUMERIC_SEARCH";
 
+    /** Feature for {@link #isFeatureSupported}. This feature covers
+     * {@link SearchSpec.Builder#setPropertyWeights}.
+     */
+    String SEARCH_SPEC_PROPERTY_WEIGHTS = "SEARCH_SPEC_PROPERTY_WEIGHTS";
+
     /**
      * Returns whether a feature is supported at run-time. Feature support depends on the
      * feature in question, the AppSearch backend being used and the Android version of the
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
index 7dae734..e7167c9 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
@@ -22,6 +22,7 @@
 import androidx.annotation.IntDef;
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
+import androidx.annotation.RequiresFeature;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.annotation.Document;
 import androidx.appsearch.exceptions.AppSearchException;
@@ -64,6 +65,7 @@
     static final String PROJECTION_TYPE_PROPERTY_PATHS_FIELD = "projectionTypeFieldMasks";
     static final String RESULT_GROUPING_TYPE_FLAGS = "resultGroupingTypeFlags";
     static final String RESULT_GROUPING_LIMIT = "resultGroupingLimit";
+    static final String TYPE_PROPERTY_WEIGHTS_FIELD = "typePropertyWeightsField";
 
     /** @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -336,6 +338,61 @@
     }
 
     /**
+     * Returns properties weights to be used for scoring.
+     *
+     * <p>Calling this function repeatedly is inefficient. Prefer to retain the {@link Map} returned
+     * by this function, rather than calling it multiple times.
+     *
+     * @return a {@link Map} of schema type to an inner-map of property paths of the schema type to
+     * the weight to set for that property.
+     */
+    @NonNull
+    public Map<String, Map<String, Double>> getPropertyWeights() {
+        Bundle typePropertyWeightsBundle = mBundle.getBundle(TYPE_PROPERTY_WEIGHTS_FIELD);
+        Set<String> schemaTypes = typePropertyWeightsBundle.keySet();
+        Map<String, Map<String, Double>> typePropertyWeightsMap = new ArrayMap<>(
+                schemaTypes.size());
+        for (String schemaType : schemaTypes) {
+            Bundle propertyPathBundle = typePropertyWeightsBundle.getBundle(schemaType);
+            Set<String> propertyPaths = propertyPathBundle.keySet();
+            Map<String, Double> propertyPathWeights = new ArrayMap<>(propertyPaths.size());
+            for (String propertyPath : propertyPaths) {
+                propertyPathWeights.put(propertyPath, propertyPathBundle.getDouble(propertyPath));
+            }
+            typePropertyWeightsMap.put(schemaType, propertyPathWeights);
+        }
+        return typePropertyWeightsMap;
+    }
+
+    /**
+     * Returns properties weights to be used for scoring.
+     *
+     * <p>Calling this function repeatedly is inefficient. Prefer to retain the {@link Map} returned
+     * by this function, rather than calling it multiple times.
+     *
+     * @return a {@link Map} of schema type to an inner-map of property paths of the schema type to
+     * the weight to set for that property.
+     */
+    @NonNull
+    public Map<String, Map<PropertyPath, Double>> getPropertyWeightPaths() {
+        Bundle typePropertyWeightsBundle = mBundle.getBundle(TYPE_PROPERTY_WEIGHTS_FIELD);
+        Set<String> schemaTypes = typePropertyWeightsBundle.keySet();
+        Map<String, Map<PropertyPath, Double>> typePropertyWeightsMap = new ArrayMap<>(
+                schemaTypes.size());
+        for (String schemaType : schemaTypes) {
+            Bundle propertyPathBundle = typePropertyWeightsBundle.getBundle(schemaType);
+            Set<String> propertyPaths = propertyPathBundle.keySet();
+            Map<PropertyPath, Double> propertyPathWeights = new ArrayMap<>(propertyPaths.size());
+            for (String propertyPath : propertyPaths) {
+                propertyPathWeights.put(new PropertyPath(propertyPath),
+                        propertyPathBundle.getDouble(propertyPath));
+            }
+            typePropertyWeightsMap.put(schemaType, propertyPathWeights);
+        }
+        return typePropertyWeightsMap;
+    }
+
+    /**
      * Get the type of grouping limit to apply, or 0 if {@link Builder#setResultGrouping} was not
      * called.
      */
@@ -359,6 +416,7 @@
         private ArrayList<String> mNamespaces = new ArrayList<>();
         private ArrayList<String> mPackageNames = new ArrayList<>();
         private Bundle mProjectionTypePropertyMasks = new Bundle();
+        private Bundle mTypePropertyWeights = new Bundle();
 
         private int mResultCountPerPage = DEFAULT_NUM_PER_PAGE;
         private @TermMatch int mTermMatchType = TERM_MATCH_PREFIX;
@@ -799,7 +857,212 @@
             return this;
         }
 
-        /** Constructs a new {@link SearchSpec} from the contents of this builder. */
+        /**
+         * Sets property weights by schema type and property path.
+         *
+         * <p>Property weights are used to promote and demote query term matches within a
+         * {@link GenericDocument} property when applying scoring.
+         *
+         * <p>Property weights must be positive values (greater than 0). A property's weight is
+         * multiplied with that property's scoring contribution. This means weights set between 0.0
+         * and 1.0 demote scoring contributions by a term match within the property. Weights set
+         * above 1.0 promote scoring contributions by a term match within the property.
+         *
+         * <p>Properties that exist in the {@link AppSearchSchema}, but do not have a weight
+         * explicitly set will be given a default weight of 1.0.
+         *
+         * <p>Weights set for property paths that do not exist in the {@link AppSearchSchema} will
+         * be discarded and not affect scoring.
+         *
+         * <p><b>NOTE:</b> Property weights only affect scoring for query-dependent scoring
+         * strategies, such as {@link #RANKING_STRATEGY_RELEVANCE_SCORE}.
+         *
+         * <!--@exportToFramework:ifJetpack()-->
+         * <p>This information may not be available depending on the backend and Android API
+         * level. To ensure it is available, call {@link Features#isFeatureSupported}.
+         * <!--@exportToFramework:else()-->
+         *
+         * @param schemaType          the schema type to set property weights for.
+         * @param propertyPathWeights a {@link Map} of property paths of the schema type to the
+         *                            weight to set for that property.
+         * @throws IllegalArgumentException if a weight is equal to or less than 0.0.
+         */
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
+        @NonNull
+        public SearchSpec.Builder setPropertyWeights(@NonNull String schemaType,
+                @NonNull Map<String, Double> propertyPathWeights) {
+            Preconditions.checkNotNull(schemaType);
+            Preconditions.checkNotNull(propertyPathWeights);
+
+            Bundle propertyPathBundle = new Bundle();
+            for (Map.Entry<String, Double> propertyPathWeightEntry :
+                    propertyPathWeights.entrySet()) {
+                String propertyPath = Preconditions.checkNotNull(propertyPathWeightEntry.getKey());
+                Double weight = Preconditions.checkNotNull(propertyPathWeightEntry.getValue());
+                if (weight <= 0.0) {
+                    throw new IllegalArgumentException("Cannot set non-positive property weight "
+                            + "value " + weight + " for property path: " + propertyPath);
+                }
+                propertyPathBundle.putDouble(propertyPath, weight);
+            }
+            mTypePropertyWeights.putBundle(schemaType, propertyPathBundle);
+            return this;
+        }
+
+        /**
+         * Sets property weights by schema type and property path.
+         *
+         * <p>Property weights are used to promote and demote query term matches within a
+         * {@link GenericDocument} property when applying scoring.
+         *
+         * <p>Property weights must be positive values (greater than 0). A property's weight is
+         * multiplied with that property's scoring contribution. This means weights set between 0.0
+         * and 1.0 demote scoring contributions by a term match within the property. Weights set
+         * above 1.0 promote scoring contributions by a term match within the property.
+         *
+         * <p>Properties that exist in the {@link AppSearchSchema}, but do not have a weight
+         * explicitly set will be given a default weight of 1.0.
+         *
+         * <p>Weights set for property paths that do not exist in the {@link AppSearchSchema} will
+         * be discarded and not affect scoring.
+         *
+         * <p><b>NOTE:</b> Property weights only affect scoring for query-dependent scoring
+         * strategies, such as {@link #RANKING_STRATEGY_RELEVANCE_SCORE}.
+         *
+         * <!--@exportToFramework:ifJetpack()-->
+         * <p>This information may not be available depending on the backend and Android API
+         * level. To ensure it is available, call {@link Features#isFeatureSupported}.
+         * <!--@exportToFramework:else()-->
+         *
+         * @param schemaType          the schema type to set property weights for.
+         * @param propertyPathWeights a {@link Map} of property paths of the schema type to the
+         *                            weight to set for that property.
+         * @throws IllegalArgumentException if a weight is equal to or less than 0.0.
+         */
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
+        @NonNull
+        public SearchSpec.Builder setPropertyWeightPaths(@NonNull String schemaType,
+                @NonNull Map<PropertyPath, Double> propertyPathWeights) {
+            Preconditions.checkNotNull(propertyPathWeights);
+
+            Map<String, Double> propertyWeights = new ArrayMap<>(propertyPathWeights.size());
+            for (Map.Entry<PropertyPath, Double> propertyPathWeightEntry :
+                    propertyPathWeights.entrySet()) {
+                PropertyPath propertyPath =
+                        Preconditions.checkNotNull(propertyPathWeightEntry.getKey());
+                propertyWeights.put(propertyPath.toString(), propertyPathWeightEntry.getValue());
+            }
+            return setPropertyWeights(schemaType, propertyWeights);
+        }
+
+// @exportToFramework:startStrip()
+
+        /**
+         * Sets property weights by schema type and property path.
+         *
+         * <p>Property weights are used to promote and demote query term matches within a
+         * {@link GenericDocument} property when applying scoring.
+         *
+         * <p>Property weights must be positive values (greater than 0). A property's weight is
+         * multiplied with that property's scoring contribution. This means weights set between 0.0
+         * and 1.0 demote scoring contributions by a term match within the property. Weights set
+         * above 1.0 promote scoring contributions by a term match within the property.
+         *
+         * <p>Properties that exist in the {@link AppSearchSchema}, but do not have a weight
+         * explicitly set will be given a default weight of 1.0.
+         *
+         * <p>Weights set for property paths that do not exist in the {@link AppSearchSchema} will
+         * be discarded and not affect scoring.
+         *
+         * <p><b>NOTE:</b> Property weights only affect scoring for query-dependent scoring
+         * strategies, such as {@link #RANKING_STRATEGY_RELEVANCE_SCORE}.
+         *
+         * <!--@exportToFramework:ifJetpack()-->
+         * <p>This information may not be available depending on the backend and Android API
+         * level. To ensure it is available, call {@link Features#isFeatureSupported}.
+         * <!--@exportToFramework:else()-->
+         *
+         * @param documentClass a class, annotated with @Document, corresponding to the schema to
+         *                      set property weights for.
+         * @param propertyPathWeights a {@link Map} of property paths of the schema type to the
+         *                            weight to set for that property.
+         * @throws AppSearchException if no factory for this document class could be found on the
+         *                            classpath
+         * @throws IllegalArgumentException if a weight is equal to or less than 0.0.
+         */
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
+        @NonNull
+        public SearchSpec.Builder setPropertyWeightsForDocumentClass(
+                @NonNull Class<?> documentClass,
+                @NonNull Map<String, Double> propertyPathWeights) throws AppSearchException {
+            Preconditions.checkNotNull(documentClass);
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+            return setPropertyWeights(factory.getSchemaName(), propertyPathWeights);
+        }
+
+        /**
+         * Sets property weights by schema type and property path.
+         *
+         * <p>Property weights are used to promote and demote query term matches within a
+         * {@link GenericDocument} property when applying scoring.
+         *
+         * <p>Property weights must be positive values (greater than 0). A property's weight is
+         * multiplied with that property's scoring contribution. This means weights set between 0.0
+         * and 1.0 demote scoring contributions by a term match within the property. Weights set
+         * above 1.0 promote scoring contributions by a term match within the property.
+         *
+         * <p>Properties that exist in the {@link AppSearchSchema}, but do not have a weight
+         * explicitly set will be given a default weight of 1.0.
+         *
+         * <p>Weights set for property paths that do not exist in the {@link AppSearchSchema} will
+         * be discarded and not affect scoring.
+         *
+         * <p><b>NOTE:</b> Property weights only affect scoring for query-dependent scoring
+         * strategies, such as {@link #RANKING_STRATEGY_RELEVANCE_SCORE}.
+         *
+         * <!--@exportToFramework:ifJetpack()-->
+         * <p>This information may not be available depending on the backend and Android API
+         * level. To ensure it is available, call {@link Features#isFeatureSupported}.
+         * <!--@exportToFramework:else()-->
+         *
+         * @param documentClass a class, annotated with @Document, corresponding to the schema to
+         *                      set property weights for.
+         * @param propertyPathWeights a {@link Map} of property paths of the schema type to the
+         *                            weight to set for that property.
+         * @throws AppSearchException if no factory for this document class could be found on the
+         *                            classpath
+         * @throws IllegalArgumentException if a weight is equal to or less than 0.0.
+         */
+        @SuppressLint("MissingGetterMatchingBuilder")
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.SEARCH_SPEC_PROPERTY_WEIGHTS)
+        @NonNull
+        public SearchSpec.Builder setPropertyWeightPathsForDocumentClass(
+                @NonNull Class<?> documentClass,
+                @NonNull Map<PropertyPath, Double> propertyPathWeights) throws AppSearchException {
+            Preconditions.checkNotNull(documentClass);
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+            return setPropertyWeightPaths(factory.getSchemaName(), propertyPathWeights);
+        }
+// @exportToFramework:endStrip()
+
+        /**
+         * Constructs a new {@link SearchSpec} from the contents of this builder.
+         *
+         * @throws IllegalArgumentException if property weights are provided with a
+         *                                  ranking strategy that isn't
+         *                                  RANKING_STRATEGY_RELEVANCE_SCORE.
+         */
         @NonNull
         public SearchSpec build() {
             Bundle bundle = new Bundle();
@@ -816,6 +1079,12 @@
             bundle.putInt(ORDER_FIELD, mOrder);
             bundle.putInt(RESULT_GROUPING_TYPE_FLAGS, mGroupingTypeFlags);
             bundle.putInt(RESULT_GROUPING_LIMIT, mGroupingLimit);
+            if (!mTypePropertyWeights.isEmpty()
+                    && RANKING_STRATEGY_RELEVANCE_SCORE != mRankingStrategy) {
+                throw new IllegalArgumentException("Property weights are only compatible with the "
+                        + "RANKING_STRATEGY_RELEVANCE_SCORE ranking strategy.");
+            }
+            bundle.putBundle(TYPE_PROPERTY_WEIGHTS_FIELD, mTypePropertyWeights);
             mBuilt = true;
             return new SearchSpec(bundle);
         }
@@ -826,6 +1095,7 @@
                 mNamespaces = new ArrayList<>(mNamespaces);
                 mPackageNames = new ArrayList<>(mPackageNames);
                 mProjectionTypePropertyMasks = BundleUtil.deepCopy(mProjectionTypePropertyMasks);
+                mTypePropertyWeights = BundleUtil.deepCopy(mTypePropertyWeights);
                 mBuilt = false;
             }
         }