1 /* 2 * Copyright 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.app.appsearch; 18 19 import android.annotation.FlaggedApi; 20 import android.annotation.IntDef; 21 import android.annotation.IntRange; 22 import android.annotation.NonNull; 23 import android.annotation.SuppressLint; 24 import android.app.appsearch.annotation.CanIgnoreReturnValue; 25 import android.app.appsearch.safeparcel.AbstractSafeParcelable; 26 import android.app.appsearch.safeparcel.SafeParcelable; 27 import android.app.appsearch.util.BundleUtil; 28 import android.os.Bundle; 29 import android.os.Parcel; 30 import android.os.Parcelable; 31 import android.util.ArrayMap; 32 import android.util.ArraySet; 33 34 import com.android.appsearch.flags.Flags; 35 import com.android.internal.util.Preconditions; 36 37 import java.lang.annotation.Retention; 38 import java.lang.annotation.RetentionPolicy; 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.Collection; 42 import java.util.Collections; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Objects; 46 import java.util.Set; 47 48 /** 49 * This class represents the specification logic for AppSearch. It can be used to set the filter and 50 * settings of search a suggestions. 51 * 52 * @see AppSearchSession#searchSuggestion 53 */ 54 @SafeParcelable.Class(creator = "SearchSuggestionSpecCreator") 55 @SuppressWarnings("HiddenSuperclass") 56 public final class SearchSuggestionSpec extends AbstractSafeParcelable { 57 58 @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) 59 @NonNull 60 public static final Parcelable.Creator<SearchSuggestionSpec> CREATOR = 61 new SearchSuggestionSpecCreator(); 62 63 @Field(id = 1, getter = "getFilterNamespaces") 64 private final List<String> mFilterNamespaces; 65 66 @Field(id = 2, getter = "getFilterSchemas") 67 private final List<String> mFilterSchemas; 68 69 // Maps are not supported by SafeParcelable fields, using Bundle instead. Here the key is 70 // schema type and value is a list of target property paths in that schema to search over. 71 @Field(id = 3) 72 final Bundle mFilterProperties; 73 74 // Maps are not supported by SafeParcelable fields, using Bundle instead. Here the key is 75 // namespace and value is a list of target document ids in that namespace to search over. 76 @Field(id = 4) 77 final Bundle mFilterDocumentIds; 78 79 @Field(id = 5, getter = "getRankingStrategy") 80 private final int mRankingStrategy; 81 82 @Field(id = 6, getter = "getMaximumResultCount") 83 private final int mMaximumResultCount; 84 85 /** @hide */ 86 @Constructor SearchSuggestionSpec( @aramid = 1) @onNull List<String> filterNamespaces, @Param(id = 2) @NonNull List<String> filterSchemas, @Param(id = 3) @NonNull Bundle filterProperties, @Param(id = 4) @NonNull Bundle filterDocumentIds, @Param(id = 5) @SuggestionRankingStrategy int rankingStrategy, @Param(id = 6) int maximumResultCount)87 public SearchSuggestionSpec( 88 @Param(id = 1) @NonNull List<String> filterNamespaces, 89 @Param(id = 2) @NonNull List<String> filterSchemas, 90 @Param(id = 3) @NonNull Bundle filterProperties, 91 @Param(id = 4) @NonNull Bundle filterDocumentIds, 92 @Param(id = 5) @SuggestionRankingStrategy int rankingStrategy, 93 @Param(id = 6) int maximumResultCount) { 94 Preconditions.checkArgument( 95 maximumResultCount >= 1, "MaximumResultCount must be positive."); 96 mFilterNamespaces = Objects.requireNonNull(filterNamespaces); 97 mFilterSchemas = Objects.requireNonNull(filterSchemas); 98 mFilterProperties = Objects.requireNonNull(filterProperties); 99 mFilterDocumentIds = Objects.requireNonNull(filterDocumentIds); 100 mRankingStrategy = rankingStrategy; 101 mMaximumResultCount = maximumResultCount; 102 } 103 104 /** 105 * Ranking Strategy for {@link SearchSuggestionResult}. 106 * 107 * @hide 108 */ 109 @IntDef( 110 value = { 111 SUGGESTION_RANKING_STRATEGY_NONE, 112 SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT, 113 SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY, 114 }) 115 @Retention(RetentionPolicy.SOURCE) 116 public @interface SuggestionRankingStrategy {} 117 118 /** 119 * Ranked by the document count that contains the term. 120 * 121 * <p>Suppose the following document is in the index. 122 * 123 * <pre>Doc1 contains: term1 term2 term2 term2</pre> 124 * 125 * <pre>Doc2 contains: term1</pre> 126 * 127 * <p>Then, suppose that a search suggestion for "t" is issued with the DOCUMENT_COUNT, the 128 * returned {@link SearchSuggestionResult}s will be: term1, term2. The term1 will have higher 129 * score and appear in the results first. 130 */ 131 public static final int SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT = 0; 132 133 /** 134 * Ranked by the term appear frequency. 135 * 136 * <p>Suppose the following document is in the index. 137 * 138 * <pre>Doc1 contains: term1 term2 term2 term2</pre> 139 * 140 * <pre>Doc2 contains: term1</pre> 141 * 142 * <p>Then, suppose that a search suggestion for "t" is issued with the TERM_FREQUENCY, the 143 * returned {@link SearchSuggestionResult}s will be: term2, term1. The term2 will have higher 144 * score and appear in the results first. 145 */ 146 public static final int SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY = 1; 147 148 /** No Ranking, results are returned in arbitrary order. */ 149 public static final int SUGGESTION_RANKING_STRATEGY_NONE = 2; 150 151 /** 152 * Returns the maximum number of wanted suggestion that will be returned in the result object. 153 */ getMaximumResultCount()154 public int getMaximumResultCount() { 155 return mMaximumResultCount; 156 } 157 158 /** 159 * Returns the list of namespaces to search over. 160 * 161 * <p>If empty, will search over all namespaces. 162 */ 163 @NonNull getFilterNamespaces()164 public List<String> getFilterNamespaces() { 165 if (mFilterNamespaces == null) { 166 return Collections.emptyList(); 167 } 168 return Collections.unmodifiableList(mFilterNamespaces); 169 } 170 171 /** Returns the ranking strategy. */ 172 @SuggestionRankingStrategy getRankingStrategy()173 public int getRankingStrategy() { 174 return mRankingStrategy; 175 } 176 177 /** 178 * Returns the list of schema to search the suggestion over. 179 * 180 * <p>If empty, will search over all schemas. 181 */ 182 @NonNull getFilterSchemas()183 public List<String> getFilterSchemas() { 184 if (mFilterSchemas == null) { 185 return Collections.emptyList(); 186 } 187 return Collections.unmodifiableList(mFilterSchemas); 188 } 189 190 /** 191 * Returns the map of schema and target properties to search over. 192 * 193 * <p>The keys of the returned map are schema types, and the values are the target property path 194 * in that schema to search over. 195 * 196 * <p>If {@link Builder#addFilterPropertyPaths} was never called, returns an empty map. In this 197 * case AppSearch will search over all schemas and properties. 198 * 199 * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this 200 * function, rather than calling it multiple times. 201 */ 202 @NonNull 203 @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) getFilterProperties()204 public Map<String, List<String>> getFilterProperties() { 205 Set<String> schemas = mFilterProperties.keySet(); 206 Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size()); 207 for (String schema : schemas) { 208 typePropertyPathsMap.put( 209 schema, Objects.requireNonNull(mFilterProperties.getStringArrayList(schema))); 210 } 211 return typePropertyPathsMap; 212 } 213 214 /** 215 * Returns the map of namespace and target document ids to search over. 216 * 217 * <p>The keys of the returned map are namespaces, and the values are the target document ids in 218 * that namespace to search over. 219 * 220 * <p>If {@link Builder#addFilterDocumentIds} was never called, returns an empty map. In this 221 * case AppSearch will search over all namespace and document ids. 222 * 223 * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this 224 * function, rather than calling it multiple times. 225 */ 226 @NonNull getFilterDocumentIds()227 public Map<String, List<String>> getFilterDocumentIds() { 228 Set<String> namespaces = mFilterDocumentIds.keySet(); 229 Map<String, List<String>> documentIdsMap = new ArrayMap<>(namespaces.size()); 230 for (String namespace : namespaces) { 231 documentIdsMap.put( 232 namespace, 233 Objects.requireNonNull(mFilterDocumentIds.getStringArrayList(namespace))); 234 } 235 return documentIdsMap; 236 } 237 238 /** Builder for {@link SearchSuggestionSpec objects}. */ 239 public static final class Builder { 240 private ArrayList<String> mNamespaces = new ArrayList<>(); 241 private ArrayList<String> mSchemas = new ArrayList<>(); 242 private Bundle mTypePropertyFilters = new Bundle(); 243 private Bundle mDocumentIds = new Bundle(); 244 private final int mTotalResultCount; 245 246 @SuggestionRankingStrategy 247 private int mRankingStrategy = SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT; 248 249 private boolean mBuilt = false; 250 251 /** 252 * Creates an {@link SearchSuggestionSpec.Builder} object. 253 * 254 * @param maximumResultCount Sets the maximum number of suggestion in the returned object. 255 */ Builder(@ntRangefrom = 1) int maximumResultCount)256 public Builder(@IntRange(from = 1) int maximumResultCount) { 257 Preconditions.checkArgument( 258 maximumResultCount >= 1, "maximumResultCount must be positive."); 259 mTotalResultCount = maximumResultCount; 260 } 261 262 /** 263 * Adds a namespace filter to {@link SearchSuggestionSpec} Entry. Only search for 264 * suggestions that has documents under the specified namespaces. 265 * 266 * <p>If unset, the query will search over all namespaces. 267 */ 268 @CanIgnoreReturnValue 269 @NonNull addFilterNamespaces(@onNull String... namespaces)270 public Builder addFilterNamespaces(@NonNull String... namespaces) { 271 Objects.requireNonNull(namespaces); 272 resetIfBuilt(); 273 return addFilterNamespaces(Arrays.asList(namespaces)); 274 } 275 276 /** 277 * Adds a namespace filter to {@link SearchSuggestionSpec} Entry. Only search for 278 * suggestions that has documents under the specified namespaces. 279 * 280 * <p>If unset, the query will search over all namespaces. 281 */ 282 @CanIgnoreReturnValue 283 @NonNull addFilterNamespaces(@onNull Collection<String> namespaces)284 public Builder addFilterNamespaces(@NonNull Collection<String> namespaces) { 285 Objects.requireNonNull(namespaces); 286 resetIfBuilt(); 287 mNamespaces.addAll(namespaces); 288 return this; 289 } 290 291 /** 292 * Sets ranking strategy for suggestion results. 293 * 294 * <p>The default value {@link #SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT} will be used if 295 * this method is never called. 296 */ 297 @CanIgnoreReturnValue 298 @NonNull setRankingStrategy(@uggestionRankingStrategy int rankingStrategy)299 public Builder setRankingStrategy(@SuggestionRankingStrategy int rankingStrategy) { 300 Preconditions.checkArgumentInRange( 301 rankingStrategy, 302 SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT, 303 SUGGESTION_RANKING_STRATEGY_NONE, 304 "Suggestion ranking strategy"); 305 resetIfBuilt(); 306 mRankingStrategy = rankingStrategy; 307 return this; 308 } 309 310 /** 311 * Adds a schema filter to {@link SearchSuggestionSpec} Entry. Only search for suggestions 312 * that has documents under the specified schema. 313 * 314 * <p>If unset, the query will search over all schema. 315 */ 316 @CanIgnoreReturnValue 317 @NonNull addFilterSchemas(@onNull String... schemaTypes)318 public Builder addFilterSchemas(@NonNull String... schemaTypes) { 319 Objects.requireNonNull(schemaTypes); 320 resetIfBuilt(); 321 return addFilterSchemas(Arrays.asList(schemaTypes)); 322 } 323 324 /** 325 * Adds a schema filter to {@link SearchSuggestionSpec} Entry. Only search for suggestions 326 * that has documents under the specified schema. 327 * 328 * <p>If unset, the query will search over all schema. 329 */ 330 @CanIgnoreReturnValue 331 @NonNull addFilterSchemas(@onNull Collection<String> schemaTypes)332 public Builder addFilterSchemas(@NonNull Collection<String> schemaTypes) { 333 Objects.requireNonNull(schemaTypes); 334 resetIfBuilt(); 335 mSchemas.addAll(schemaTypes); 336 return this; 337 } 338 339 /** 340 * Adds property paths for the specified type to the property filter of {@link 341 * SearchSuggestionSpec} Entry. Only search for suggestions that has content under the 342 * specified property. If property paths are added for a type, then only the properties 343 * referred to will be retrieved for results of that type. 344 * 345 * <p>If a property path that is specified isn't present in a result, it will be ignored for 346 * that result. Property paths cannot be null. 347 * 348 * <p>If no property paths are added for a particular type, then all properties of results 349 * of that type will be retrieved. 350 * 351 * <p>Example properties: 'body', 'sender.name', 'sender.emailaddress', etc. 352 * 353 * @param schema the {@link AppSearchSchema} that contains the target properties 354 * @param propertyPaths The String version of {@link PropertyPath}. A dot-delimited sequence 355 * of property names indicating which property in the document these snippets correspond 356 * to. 357 */ 358 @CanIgnoreReturnValue 359 @NonNull 360 @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) addFilterProperties( @onNull String schema, @NonNull Collection<String> propertyPaths)361 public Builder addFilterProperties( 362 @NonNull String schema, @NonNull Collection<String> propertyPaths) { 363 Objects.requireNonNull(schema); 364 Objects.requireNonNull(propertyPaths); 365 resetIfBuilt(); 366 ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size()); 367 for (String propertyPath : propertyPaths) { 368 Objects.requireNonNull(propertyPath); 369 propertyPathsArrayList.add(propertyPath); 370 } 371 mTypePropertyFilters.putStringArrayList(schema, propertyPathsArrayList); 372 return this; 373 } 374 375 /** 376 * Adds property paths for the specified type to the property filter of {@link 377 * SearchSuggestionSpec} Entry. Only search for suggestions that has content under the 378 * specified property. If property paths are added for a type, then only the properties 379 * referred to will be retrieved for results of that type. 380 * 381 * <p>If a property path that is specified isn't present in a result, it will be ignored for 382 * that result. Property paths cannot be null. 383 * 384 * <p>If no property paths are added for a particular type, then all properties of results 385 * of that type will be retrieved. 386 * 387 * @param schema the {@link AppSearchSchema} that contains the target properties 388 * @param propertyPaths The {@link PropertyPath} to search suggestion over 389 */ 390 @CanIgnoreReturnValue 391 @NonNull 392 // Getter method is getFilterProperties 393 @SuppressLint("MissingGetterMatchingBuilder") 394 @FlaggedApi(Flags.FLAG_ENABLE_SEARCH_SPEC_FILTER_PROPERTIES) addFilterPropertyPaths( @onNull String schema, @NonNull Collection<PropertyPath> propertyPaths)395 public Builder addFilterPropertyPaths( 396 @NonNull String schema, @NonNull Collection<PropertyPath> propertyPaths) { 397 Objects.requireNonNull(schema); 398 Objects.requireNonNull(propertyPaths); 399 ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size()); 400 for (PropertyPath propertyPath : propertyPaths) { 401 propertyPathsArrayList.add(propertyPath.toString()); 402 } 403 return addFilterProperties(schema, propertyPathsArrayList); 404 } 405 406 /** 407 * Adds a document ID filter to {@link SearchSuggestionSpec} Entry. Only search for 408 * suggestions in the given specified documents. 409 * 410 * <p>If unset, the query will search over all documents. 411 */ 412 @CanIgnoreReturnValue 413 @NonNull addFilterDocumentIds( @onNull String namespace, @NonNull String... documentIds)414 public Builder addFilterDocumentIds( 415 @NonNull String namespace, @NonNull String... documentIds) { 416 Objects.requireNonNull(namespace); 417 Objects.requireNonNull(documentIds); 418 resetIfBuilt(); 419 return addFilterDocumentIds(namespace, Arrays.asList(documentIds)); 420 } 421 422 /** 423 * Adds a document ID filter to {@link SearchSuggestionSpec} Entry. Only search for 424 * suggestions in the given specified documents. 425 * 426 * <p>If unset, the query will search over all documents. 427 */ 428 @CanIgnoreReturnValue 429 @NonNull addFilterDocumentIds( @onNull String namespace, @NonNull Collection<String> documentIds)430 public Builder addFilterDocumentIds( 431 @NonNull String namespace, @NonNull Collection<String> documentIds) { 432 Objects.requireNonNull(namespace); 433 Objects.requireNonNull(documentIds); 434 resetIfBuilt(); 435 ArrayList<String> documentIdList = new ArrayList<>(documentIds.size()); 436 for (String documentId : documentIds) { 437 documentIdList.add(Objects.requireNonNull(documentId)); 438 } 439 mDocumentIds.putStringArrayList(namespace, documentIdList); 440 return this; 441 } 442 443 /** Constructs a new {@link SearchSpec} from the contents of this builder. */ 444 @NonNull build()445 public SearchSuggestionSpec build() { 446 if (!mSchemas.isEmpty()) { 447 Set<String> schemaFilter = new ArraySet<>(mSchemas); 448 for (String schema : mTypePropertyFilters.keySet()) { 449 if (!schemaFilter.contains(schema)) { 450 throw new IllegalStateException( 451 "The schema: " 452 + schema 453 + " exists in the property filter but " 454 + "doesn't exist in the schema filter."); 455 } 456 } 457 } 458 if (!mNamespaces.isEmpty()) { 459 Set<String> namespaceFilter = new ArraySet<>(mNamespaces); 460 for (String namespace : mDocumentIds.keySet()) { 461 if (!namespaceFilter.contains(namespace)) { 462 throw new IllegalStateException( 463 "The namespace: " 464 + namespace 465 + " exists in the document id " 466 + "filter but doesn't exist in the namespace filter."); 467 } 468 } 469 } 470 mBuilt = true; 471 return new SearchSuggestionSpec( 472 mNamespaces, 473 mSchemas, 474 mTypePropertyFilters, 475 mDocumentIds, 476 mRankingStrategy, 477 mTotalResultCount); 478 } 479 resetIfBuilt()480 private void resetIfBuilt() { 481 if (mBuilt) { 482 mNamespaces = new ArrayList<>(mNamespaces); 483 mSchemas = new ArrayList<>(mSchemas); 484 mTypePropertyFilters = BundleUtil.deepCopy(mTypePropertyFilters); 485 mDocumentIds = BundleUtil.deepCopy(mDocumentIds); 486 mBuilt = false; 487 } 488 } 489 } 490 491 @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) 492 @Override writeToParcel(@onNull Parcel dest, int flags)493 public void writeToParcel(@NonNull Parcel dest, int flags) { 494 SearchSuggestionSpecCreator.writeToParcel(this, dest, flags); 495 } 496 } 497