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;
}
}