1 /* 2 * Copyright 2020 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.IntDef; 20 import android.annotation.IntRange; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.SuppressLint; 24 import android.app.appsearch.annotation.CanIgnoreReturnValue; 25 import android.app.appsearch.exceptions.AppSearchException; 26 import android.app.appsearch.util.BundleUtil; 27 import android.os.Bundle; 28 import android.util.ArrayMap; 29 import android.util.ArraySet; 30 31 import com.android.internal.util.Preconditions; 32 33 import java.lang.annotation.Retention; 34 import java.lang.annotation.RetentionPolicy; 35 import java.util.ArrayList; 36 import java.util.Arrays; 37 import java.util.Collection; 38 import java.util.Collections; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Objects; 42 import java.util.Set; 43 44 /** 45 * This class represents the specification logic for AppSearch. It can be used to set the type of 46 * search, like prefix or exact only or apply filters to search for a specific schema type only etc. 47 */ 48 public final class SearchSpec { 49 /** 50 * Schema type to be used in {@link SearchSpec.Builder#addProjection} to apply property paths to 51 * all results, excepting any types that have had their own, specific property paths set. 52 */ 53 public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*"; 54 55 static final String TERM_MATCH_TYPE_FIELD = "termMatchType"; 56 static final String SCHEMA_FIELD = "schema"; 57 static final String NAMESPACE_FIELD = "namespace"; 58 static final String PACKAGE_NAME_FIELD = "packageName"; 59 static final String NUM_PER_PAGE_FIELD = "numPerPage"; 60 static final String RANKING_STRATEGY_FIELD = "rankingStrategy"; 61 static final String ORDER_FIELD = "order"; 62 static final String SNIPPET_COUNT_FIELD = "snippetCount"; 63 static final String SNIPPET_COUNT_PER_PROPERTY_FIELD = "snippetCountPerProperty"; 64 static final String MAX_SNIPPET_FIELD = "maxSnippet"; 65 static final String PROJECTION_TYPE_PROPERTY_PATHS_FIELD = "projectionTypeFieldMasks"; 66 static final String RESULT_GROUPING_TYPE_FLAGS = "resultGroupingTypeFlags"; 67 static final String RESULT_GROUPING_LIMIT = "resultGroupingLimit"; 68 static final String TYPE_PROPERTY_WEIGHTS_FIELD = "typePropertyWeightsField"; 69 static final String JOIN_SPEC = "joinSpec"; 70 static final String ADVANCED_RANKING_EXPRESSION = "advancedRankingExpression"; 71 static final String ENABLED_FEATURES_FIELD = "enabledFeatures"; 72 73 /** @hide */ 74 public static final int DEFAULT_NUM_PER_PAGE = 10; 75 76 // TODO(b/170371356): In framework, we may want these limits to be flag controlled. 77 // If that happens, the @IntRange() directives in this class may have to change. 78 private static final int MAX_NUM_PER_PAGE = 10_000; 79 private static final int MAX_SNIPPET_COUNT = 10_000; 80 private static final int MAX_SNIPPET_PER_PROPERTY_COUNT = 10_000; 81 private static final int MAX_SNIPPET_SIZE_LIMIT = 10_000; 82 83 /** 84 * Term Match Type for the query. 85 * 86 * @hide 87 */ 88 // NOTE: The integer values of these constants must match the proto enum constants in 89 // {@link com.google.android.icing.proto.SearchSpecProto.termMatchType} 90 @IntDef(value = {TERM_MATCH_EXACT_ONLY, TERM_MATCH_PREFIX}) 91 @Retention(RetentionPolicy.SOURCE) 92 public @interface TermMatch {} 93 94 /** 95 * Query terms will only match exact tokens in the index. 96 * 97 * <p>For example, a query term "foo" will only match indexed token "foo", and not "foot" or 98 * "football". 99 */ 100 public static final int TERM_MATCH_EXACT_ONLY = 1; 101 /** 102 * Query terms will match indexed tokens when the query term is a prefix of the token. 103 * 104 * <p>For example, a query term "foo" will match indexed tokens like "foo", "foot", and 105 * "football". 106 */ 107 public static final int TERM_MATCH_PREFIX = 2; 108 109 /** 110 * Ranking Strategy for query result. 111 * 112 * @hide 113 */ 114 // NOTE: The integer values of these constants must match the proto enum constants in 115 // {@link ScoringSpecProto.RankingStrategy.Code} 116 @IntDef( 117 value = { 118 RANKING_STRATEGY_NONE, 119 RANKING_STRATEGY_DOCUMENT_SCORE, 120 RANKING_STRATEGY_CREATION_TIMESTAMP, 121 RANKING_STRATEGY_RELEVANCE_SCORE, 122 RANKING_STRATEGY_USAGE_COUNT, 123 RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP, 124 RANKING_STRATEGY_SYSTEM_USAGE_COUNT, 125 RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP, 126 RANKING_STRATEGY_JOIN_AGGREGATE_SCORE, 127 RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION, 128 }) 129 @Retention(RetentionPolicy.SOURCE) 130 public @interface RankingStrategy {} 131 132 /** No Ranking, results are returned in arbitrary order. */ 133 public static final int RANKING_STRATEGY_NONE = 0; 134 /** Ranked by app-provided document scores. */ 135 public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; 136 /** Ranked by document creation timestamps. */ 137 public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2; 138 /** Ranked by document relevance score. */ 139 public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3; 140 /** Ranked by number of usages, as reported by the app. */ 141 public static final int RANKING_STRATEGY_USAGE_COUNT = 4; 142 /** Ranked by timestamp of last usage, as reported by the app. */ 143 public static final int RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP = 5; 144 /** Ranked by number of usages from a system UI surface. */ 145 public static final int RANKING_STRATEGY_SYSTEM_USAGE_COUNT = 6; 146 /** Ranked by timestamp of last usage from a system UI surface. */ 147 public static final int RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP = 7; 148 /** 149 * Ranked by the aggregated ranking signal of the joined documents. 150 * 151 * <p>Which aggregation strategy is used to determine a ranking signal is specified in the 152 * {@link JoinSpec} set by {@link Builder#setJoinSpec}. This ranking strategy may not be used if 153 * no {@link JoinSpec} is provided. 154 * 155 * @see Builder#build 156 */ 157 public static final int RANKING_STRATEGY_JOIN_AGGREGATE_SCORE = 8; 158 /** Ranked by the advanced ranking expression provided. */ 159 public static final int RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION = 9; 160 161 /** 162 * Order for query result. 163 * 164 * @hide 165 */ 166 // NOTE: The integer values of these constants must match the proto enum constants in 167 // {@link ScoringSpecProto.Order.Code} 168 @IntDef(value = {ORDER_DESCENDING, ORDER_ASCENDING}) 169 @Retention(RetentionPolicy.SOURCE) 170 public @interface Order {} 171 172 /** Search results will be returned in a descending order. */ 173 public static final int ORDER_DESCENDING = 0; 174 /** Search results will be returned in an ascending order. */ 175 public static final int ORDER_ASCENDING = 1; 176 177 /** 178 * Grouping type for result limits. 179 * 180 * @hide 181 */ 182 @IntDef( 183 flag = true, 184 value = { 185 GROUPING_TYPE_PER_PACKAGE, 186 GROUPING_TYPE_PER_NAMESPACE, 187 GROUPING_TYPE_PER_SCHEMA 188 }) 189 @Retention(RetentionPolicy.SOURCE) 190 public @interface GroupingType {} 191 /** 192 * Results should be grouped together by package for the purpose of enforcing a limit on the 193 * number of results returned per package. 194 */ 195 public static final int GROUPING_TYPE_PER_PACKAGE = 1 << 0; 196 /** 197 * Results should be grouped together by namespace for the purpose of enforcing a limit on the 198 * number of results returned per namespace. 199 */ 200 public static final int GROUPING_TYPE_PER_NAMESPACE = 1 << 1; 201 /** 202 * Results should be grouped together by schema type for the purpose of enforcing a limit on the 203 * number of results returned per schema type. 204 * 205 * @hide 206 */ 207 public static final int GROUPING_TYPE_PER_SCHEMA = 1 << 2; 208 209 private final Bundle mBundle; 210 211 /** @hide */ SearchSpec(@onNull Bundle bundle)212 public SearchSpec(@NonNull Bundle bundle) { 213 Objects.requireNonNull(bundle); 214 mBundle = bundle; 215 } 216 217 /** 218 * Returns the {@link Bundle} populated by this builder. 219 * 220 * @hide 221 */ 222 @NonNull getBundle()223 public Bundle getBundle() { 224 return mBundle; 225 } 226 227 /** Returns how the query terms should match terms in the index. */ 228 @TermMatch getTermMatch()229 public int getTermMatch() { 230 return mBundle.getInt(TERM_MATCH_TYPE_FIELD, -1); 231 } 232 233 /** 234 * Returns the list of schema types to search for. 235 * 236 * <p>If empty, the query will search over all schema types. 237 */ 238 @NonNull getFilterSchemas()239 public List<String> getFilterSchemas() { 240 List<String> schemas = mBundle.getStringArrayList(SCHEMA_FIELD); 241 if (schemas == null) { 242 return Collections.emptyList(); 243 } 244 return Collections.unmodifiableList(schemas); 245 } 246 247 /** 248 * Returns the list of namespaces to search over. 249 * 250 * <p>If empty, the query will search over all namespaces. 251 */ 252 @NonNull getFilterNamespaces()253 public List<String> getFilterNamespaces() { 254 List<String> namespaces = mBundle.getStringArrayList(NAMESPACE_FIELD); 255 if (namespaces == null) { 256 return Collections.emptyList(); 257 } 258 return Collections.unmodifiableList(namespaces); 259 } 260 261 /** 262 * Returns the list of package name filters to search over. 263 * 264 * <p>If empty, the query will search over all packages that the caller has access to. If 265 * package names are specified which caller doesn't have access to, then those package names 266 * will be ignored. 267 */ 268 @NonNull getFilterPackageNames()269 public List<String> getFilterPackageNames() { 270 List<String> packageNames = mBundle.getStringArrayList(PACKAGE_NAME_FIELD); 271 if (packageNames == null) { 272 return Collections.emptyList(); 273 } 274 return Collections.unmodifiableList(packageNames); 275 } 276 277 /** Returns the number of results per page in the result set. */ getResultCountPerPage()278 public int getResultCountPerPage() { 279 return mBundle.getInt(NUM_PER_PAGE_FIELD, DEFAULT_NUM_PER_PAGE); 280 } 281 282 /** Returns the ranking strategy. */ 283 @RankingStrategy getRankingStrategy()284 public int getRankingStrategy() { 285 return mBundle.getInt(RANKING_STRATEGY_FIELD); 286 } 287 288 /** Returns the order of returned search results (descending or ascending). */ 289 @Order getOrder()290 public int getOrder() { 291 return mBundle.getInt(ORDER_FIELD); 292 } 293 294 /** Returns how many documents to generate snippets for. */ getSnippetCount()295 public int getSnippetCount() { 296 return mBundle.getInt(SNIPPET_COUNT_FIELD); 297 } 298 299 /** 300 * Returns how many matches for each property of a matching document to generate snippets for. 301 */ getSnippetCountPerProperty()302 public int getSnippetCountPerProperty() { 303 return mBundle.getInt(SNIPPET_COUNT_PER_PROPERTY_FIELD); 304 } 305 306 /** Returns the maximum size of a snippet in characters. */ getMaxSnippetSize()307 public int getMaxSnippetSize() { 308 return mBundle.getInt(MAX_SNIPPET_FIELD); 309 } 310 311 /** 312 * Returns a map from schema type to property paths to be used for projection. 313 * 314 * <p>If the map is empty, then all properties will be retrieved for all results. 315 * 316 * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this 317 * function, rather than calling it multiple times. 318 * 319 * @return A mapping of schema types to lists of projection strings. 320 */ 321 @NonNull getProjections()322 public Map<String, List<String>> getProjections() { 323 Bundle typePropertyPathsBundle = 324 Objects.requireNonNull(mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD)); 325 Set<String> schemas = typePropertyPathsBundle.keySet(); 326 Map<String, List<String>> typePropertyPathsMap = new ArrayMap<>(schemas.size()); 327 for (String schema : schemas) { 328 typePropertyPathsMap.put( 329 schema, 330 Objects.requireNonNull(typePropertyPathsBundle.getStringArrayList(schema))); 331 } 332 return typePropertyPathsMap; 333 } 334 335 /** 336 * Returns a map from schema type to property paths to be used for projection. 337 * 338 * <p>If the map is empty, then all properties will be retrieved for all results. 339 * 340 * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned by this 341 * function, rather than calling it multiple times. 342 * 343 * @return A mapping of schema types to lists of projection {@link PropertyPath} objects. 344 */ 345 @NonNull getProjectionPaths()346 public Map<String, List<PropertyPath>> getProjectionPaths() { 347 Bundle typePropertyPathsBundle = mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD); 348 Set<String> schemas = typePropertyPathsBundle.keySet(); 349 Map<String, List<PropertyPath>> typePropertyPathsMap = new ArrayMap<>(schemas.size()); 350 for (String schema : schemas) { 351 ArrayList<String> propertyPathList = typePropertyPathsBundle.getStringArrayList(schema); 352 List<PropertyPath> copy = new ArrayList<>(propertyPathList.size()); 353 for (String p : propertyPathList) { 354 copy.add(new PropertyPath(p)); 355 } 356 typePropertyPathsMap.put(schema, copy); 357 } 358 return typePropertyPathsMap; 359 } 360 361 /** 362 * Returns properties weights to be used for scoring. 363 * 364 * <p>Calling this function repeatedly is inefficient. Prefer to retain the {@link Map} returned 365 * by this function, rather than calling it multiple times. 366 * 367 * @return a {@link Map} of schema type to an inner-map of property paths of the schema type to 368 * the weight to set for that property. 369 */ 370 @NonNull getPropertyWeights()371 public Map<String, Map<String, Double>> getPropertyWeights() { 372 Bundle typePropertyWeightsBundle = mBundle.getBundle(TYPE_PROPERTY_WEIGHTS_FIELD); 373 Set<String> schemaTypes = typePropertyWeightsBundle.keySet(); 374 Map<String, Map<String, Double>> typePropertyWeightsMap = 375 new ArrayMap<>(schemaTypes.size()); 376 for (String schemaType : schemaTypes) { 377 Bundle propertyPathBundle = typePropertyWeightsBundle.getBundle(schemaType); 378 Set<String> propertyPaths = propertyPathBundle.keySet(); 379 Map<String, Double> propertyPathWeights = new ArrayMap<>(propertyPaths.size()); 380 for (String propertyPath : propertyPaths) { 381 propertyPathWeights.put(propertyPath, propertyPathBundle.getDouble(propertyPath)); 382 } 383 typePropertyWeightsMap.put(schemaType, propertyPathWeights); 384 } 385 return typePropertyWeightsMap; 386 } 387 388 /** 389 * Returns properties weights to be used for scoring. 390 * 391 * <p>Calling this function repeatedly is inefficient. Prefer to retain the {@link Map} returned 392 * by this function, rather than calling it multiple times. 393 * 394 * @return a {@link Map} of schema type to an inner-map of property paths of the schema type to 395 * the weight to set for that property. 396 */ 397 @NonNull getPropertyWeightPaths()398 public Map<String, Map<PropertyPath, Double>> getPropertyWeightPaths() { 399 Bundle typePropertyWeightsBundle = mBundle.getBundle(TYPE_PROPERTY_WEIGHTS_FIELD); 400 Set<String> schemaTypes = typePropertyWeightsBundle.keySet(); 401 Map<String, Map<PropertyPath, Double>> typePropertyWeightsMap = 402 new ArrayMap<>(schemaTypes.size()); 403 for (String schemaType : schemaTypes) { 404 Bundle propertyPathBundle = typePropertyWeightsBundle.getBundle(schemaType); 405 Set<String> propertyPaths = propertyPathBundle.keySet(); 406 Map<PropertyPath, Double> propertyPathWeights = new ArrayMap<>(propertyPaths.size()); 407 for (String propertyPath : propertyPaths) { 408 propertyPathWeights.put( 409 new PropertyPath(propertyPath), propertyPathBundle.getDouble(propertyPath)); 410 } 411 typePropertyWeightsMap.put(schemaType, propertyPathWeights); 412 } 413 return typePropertyWeightsMap; 414 } 415 416 /** 417 * Get the type of grouping limit to apply, or 0 if {@link Builder#setResultGrouping} was not 418 * called. 419 */ 420 @GroupingType getResultGroupingTypeFlags()421 public int getResultGroupingTypeFlags() { 422 return mBundle.getInt(RESULT_GROUPING_TYPE_FLAGS); 423 } 424 425 /** 426 * Get the maximum number of results to return for each group. 427 * 428 * @return the maximum number of results to return for each group or Integer.MAX_VALUE if {@link 429 * Builder#setResultGrouping(int, int)} was not called. 430 */ getResultGroupingLimit()431 public int getResultGroupingLimit() { 432 return mBundle.getInt(RESULT_GROUPING_LIMIT, Integer.MAX_VALUE); 433 } 434 435 /** Returns specification on which documents need to be joined. */ 436 @Nullable getJoinSpec()437 public JoinSpec getJoinSpec() { 438 Bundle joinSpec = mBundle.getBundle(JOIN_SPEC); 439 if (joinSpec == null) { 440 return null; 441 } 442 return new JoinSpec(joinSpec); 443 } 444 445 /** 446 * Get the advanced ranking expression, or "" if {@link Builder#setRankingStrategy(String)} was 447 * not called. 448 */ 449 @NonNull getAdvancedRankingExpression()450 public String getAdvancedRankingExpression() { 451 return mBundle.getString(ADVANCED_RANKING_EXPRESSION, ""); 452 } 453 454 /** Returns whether the {@link Features#NUMERIC_SEARCH} feature is enabled. */ isNumericSearchEnabled()455 public boolean isNumericSearchEnabled() { 456 return getEnabledFeatures().contains(FeatureConstants.NUMERIC_SEARCH); 457 } 458 459 /** Returns whether the {@link Features#VERBATIM_SEARCH} feature is enabled. */ isVerbatimSearchEnabled()460 public boolean isVerbatimSearchEnabled() { 461 return getEnabledFeatures().contains(FeatureConstants.VERBATIM_SEARCH); 462 } 463 464 /** Returns whether the {@link Features#LIST_FILTER_QUERY_LANGUAGE} feature is enabled. */ isListFilterQueryLanguageEnabled()465 public boolean isListFilterQueryLanguageEnabled() { 466 return getEnabledFeatures().contains(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE); 467 } 468 469 /** 470 * Get the list of enabled features that the caller is intending to use in this search call. 471 * 472 * @return the set of {@link Features} enabled in this {@link SearchSpec} Entry. 473 * @hide 474 */ 475 @NonNull getEnabledFeatures()476 public List<String> getEnabledFeatures() { 477 return mBundle.getStringArrayList(ENABLED_FEATURES_FIELD); 478 } 479 480 /** Builder for {@link SearchSpec objects}. */ 481 public static final class Builder { 482 private ArrayList<String> mSchemas = new ArrayList<>(); 483 private ArrayList<String> mNamespaces = new ArrayList<>(); 484 private ArrayList<String> mPackageNames = new ArrayList<>(); 485 private ArraySet<String> mEnabledFeatures = new ArraySet<>(); 486 private Bundle mProjectionTypePropertyMasks = new Bundle(); 487 private Bundle mTypePropertyWeights = new Bundle(); 488 489 private int mResultCountPerPage = DEFAULT_NUM_PER_PAGE; 490 @TermMatch private int mTermMatchType = TERM_MATCH_PREFIX; 491 private int mSnippetCount = 0; 492 private int mSnippetCountPerProperty = MAX_SNIPPET_PER_PROPERTY_COUNT; 493 private int mMaxSnippetSize = 0; 494 @RankingStrategy private int mRankingStrategy = RANKING_STRATEGY_NONE; 495 @Order private int mOrder = ORDER_DESCENDING; 496 @GroupingType private int mGroupingTypeFlags = 0; 497 private int mGroupingLimit = 0; 498 private JoinSpec mJoinSpec; 499 private String mAdvancedRankingExpression = ""; 500 private boolean mBuilt = false; 501 502 /** 503 * Sets how the query terms should match {@code TermMatchCode} in the index. 504 * 505 * <p>If this method is not called, the default term match type is {@link 506 * SearchSpec#TERM_MATCH_PREFIX}. 507 */ 508 @CanIgnoreReturnValue 509 @NonNull setTermMatch(@ermMatch int termMatchType)510 public Builder setTermMatch(@TermMatch int termMatchType) { 511 Preconditions.checkArgumentInRange( 512 termMatchType, TERM_MATCH_EXACT_ONLY, TERM_MATCH_PREFIX, "Term match type"); 513 resetIfBuilt(); 514 mTermMatchType = termMatchType; 515 return this; 516 } 517 518 /** 519 * Adds a Schema type filter to {@link SearchSpec} Entry. Only search for documents that 520 * have the specified schema types. 521 * 522 * <p>If unset, the query will search over all schema types. 523 */ 524 @CanIgnoreReturnValue 525 @NonNull addFilterSchemas(@onNull String... schemas)526 public Builder addFilterSchemas(@NonNull String... schemas) { 527 Objects.requireNonNull(schemas); 528 resetIfBuilt(); 529 return addFilterSchemas(Arrays.asList(schemas)); 530 } 531 532 /** 533 * Adds a Schema type filter to {@link SearchSpec} Entry. Only search for documents that 534 * have the specified schema types. 535 * 536 * <p>If unset, the query will search over all schema types. 537 */ 538 @CanIgnoreReturnValue 539 @NonNull addFilterSchemas(@onNull Collection<String> schemas)540 public Builder addFilterSchemas(@NonNull Collection<String> schemas) { 541 Objects.requireNonNull(schemas); 542 resetIfBuilt(); 543 mSchemas.addAll(schemas); 544 return this; 545 } 546 547 /** 548 * Adds a namespace filter to {@link SearchSpec} Entry. Only search for documents that have 549 * the specified namespaces. 550 * 551 * <p>If unset, the query will search over all namespaces. 552 */ 553 @CanIgnoreReturnValue 554 @NonNull addFilterNamespaces(@onNull String... namespaces)555 public Builder addFilterNamespaces(@NonNull String... namespaces) { 556 Objects.requireNonNull(namespaces); 557 resetIfBuilt(); 558 return addFilterNamespaces(Arrays.asList(namespaces)); 559 } 560 561 /** 562 * Adds a namespace filter to {@link SearchSpec} Entry. Only search for documents that have 563 * the specified namespaces. 564 * 565 * <p>If unset, the query will search over all namespaces. 566 */ 567 @CanIgnoreReturnValue 568 @NonNull addFilterNamespaces(@onNull Collection<String> namespaces)569 public Builder addFilterNamespaces(@NonNull Collection<String> namespaces) { 570 Objects.requireNonNull(namespaces); 571 resetIfBuilt(); 572 mNamespaces.addAll(namespaces); 573 return this; 574 } 575 576 /** 577 * Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that 578 * were indexed from the specified packages. 579 * 580 * <p>If unset, the query will search over all packages that the caller has access to. If 581 * package names are specified which caller doesn't have access to, then those package names 582 * will be ignored. 583 */ 584 @CanIgnoreReturnValue 585 @NonNull addFilterPackageNames(@onNull String... packageNames)586 public Builder addFilterPackageNames(@NonNull String... packageNames) { 587 Objects.requireNonNull(packageNames); 588 resetIfBuilt(); 589 return addFilterPackageNames(Arrays.asList(packageNames)); 590 } 591 592 /** 593 * Adds a package name filter to {@link SearchSpec} Entry. Only search for documents that 594 * were indexed from the specified packages. 595 * 596 * <p>If unset, the query will search over all packages that the caller has access to. If 597 * package names are specified which caller doesn't have access to, then those package names 598 * will be ignored. 599 */ 600 @CanIgnoreReturnValue 601 @NonNull addFilterPackageNames(@onNull Collection<String> packageNames)602 public Builder addFilterPackageNames(@NonNull Collection<String> packageNames) { 603 Objects.requireNonNull(packageNames); 604 resetIfBuilt(); 605 mPackageNames.addAll(packageNames); 606 return this; 607 } 608 609 /** 610 * Sets the number of results per page in the returned object. 611 * 612 * <p>The default number of results per page is 10. 613 */ 614 @CanIgnoreReturnValue 615 @NonNull setResultCountPerPage( @ntRangefrom = 0, to = MAX_NUM_PER_PAGE) int resultCountPerPage)616 public SearchSpec.Builder setResultCountPerPage( 617 @IntRange(from = 0, to = MAX_NUM_PER_PAGE) int resultCountPerPage) { 618 Preconditions.checkArgumentInRange( 619 resultCountPerPage, 0, MAX_NUM_PER_PAGE, "resultCountPerPage"); 620 resetIfBuilt(); 621 mResultCountPerPage = resultCountPerPage; 622 return this; 623 } 624 625 /** Sets ranking strategy for AppSearch results. */ 626 @CanIgnoreReturnValue 627 @NonNull setRankingStrategy(@ankingStrategy int rankingStrategy)628 public Builder setRankingStrategy(@RankingStrategy int rankingStrategy) { 629 Preconditions.checkArgumentInRange( 630 rankingStrategy, 631 RANKING_STRATEGY_NONE, 632 RANKING_STRATEGY_JOIN_AGGREGATE_SCORE, 633 "Result ranking strategy"); 634 resetIfBuilt(); 635 mRankingStrategy = rankingStrategy; 636 mAdvancedRankingExpression = ""; 637 return this; 638 } 639 640 /** 641 * Enables advanced ranking to score based on {@code advancedRankingExpression}. 642 * 643 * <p>This method will set RankingStrategy to {@link 644 * #RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION}. 645 * 646 * <p>The ranking expression is a mathematical expression that will be evaluated to a 647 * floating-point number of double type representing the score of each document. 648 * 649 * <p>Numeric literals, arithmetic operators, mathematical functions, and document-based 650 * functions are supported to build expressions. 651 * 652 * <p>The following are supported arithmetic operators: 653 * 654 * <ul> 655 * <li>Addition(+) 656 * <li>Subtraction(-) 657 * <li>Multiplication(*) 658 * <li>Floating Point Division(/) 659 * </ul> 660 * 661 * <p>Operator precedences are compliant with the Java Language, and parentheses are 662 * supported. For example, "2.2 + (3 - 4) / 2" evaluates to 1.7. 663 * 664 * <p>The following are supported basic mathematical functions: 665 * 666 * <ul> 667 * <li>log(x) - the natural log of x 668 * <li>log(x, y) - the log of y with base x 669 * <li>pow(x, y) - x to the power of y 670 * <li>sqrt(x) 671 * <li>abs(x) 672 * <li>sin(x), cos(x), tan(x) 673 * <li>Example: "max(abs(-100), 10) + pow(2, 10)" will be evaluated to 1124 674 * </ul> 675 * 676 * <p>The following variadic mathematical functions are supported, with n > 0. They also 677 * accept list value parameters. For example, if V is a value of list type, we can call 678 * sum(V) to get the sum of all the values in V. List literals are not supported, so a value 679 * of list type can only be constructed as a return value of some particular document-based 680 * functions. 681 * 682 * <ul> 683 * <li>max(v1, v2, ..., vn) or max(V) 684 * <li>min(v1, v2, ..., vn) or min(V) 685 * <li>len(v1, v2, ..., vn) or len(V) 686 * <li>sum(v1, v2, ..., vn) or sum(V) 687 * <li>avg(v1, v2, ..., vn) or avg(V) 688 * </ul> 689 * 690 * <p>Document-based functions must be called via "this", which represents the current 691 * document being scored. The following are supported document-based functions: 692 * 693 * <ul> 694 * <li>this.documentScore() 695 * <p>Get the app-provided document score of the current document. This is the same 696 * score that is returned for {@link #RANKING_STRATEGY_DOCUMENT_SCORE}. 697 * <li>this.creationTimestamp() 698 * <p>Get the creation timestamp of the current document. This is the same score that 699 * is returned for {@link #RANKING_STRATEGY_CREATION_TIMESTAMP}. 700 * <li>this.relevanceScore() 701 * <p>Get the BM25F relevance score of the current document in relation to the query 702 * string. This is the same score that is returned for {@link 703 * #RANKING_STRATEGY_RELEVANCE_SCORE}. 704 * <li>this.usageCount(type) and this.usageLastUsedTimestamp(type) 705 * <p>Get the number of usages or the timestamp of last usage by type for the current 706 * document, where type must be evaluated to an integer from 1 to 2. Type 1 refers to 707 * usages reported by {@link AppSearchSession#reportUsage}, and type 2 refers to 708 * usages reported by {@link GlobalSearchSession#reportSystemUsage}. 709 * <li>this.childrenRankingSignals() 710 * <p>Returns a list of children ranking signals calculated by scoring the joined 711 * documents using the ranking strategy specified in the nested {@link SearchSpec}. 712 * Currently, a document can only be a child of another document in the context of 713 * joins. If this function is called without the Join API enabled, a type error will 714 * be raised. 715 * <li>this.propertyWeights() 716 * <p>Returns a list of the normalized weights of the matched properties for the 717 * current document being scored. Property weights come from what's specified in 718 * {@link SearchSpec}. After normalizing, each provided weight will be divided by the 719 * maximum weight, so that each of them will be <= 1. 720 * </ul> 721 * 722 * <p>Some errors may occur when using advanced ranking. 723 * 724 * <p>Syntax Error: the expression violates the syntax of the advanced ranking language. 725 * Below are some examples. 726 * 727 * <ul> 728 * <li>"1 + " - missing operand 729 * <li>"2 * (1 + 2))" - unbalanced parenthesis 730 * <li>"2 ^ 3" - unknown operator 731 * </ul> 732 * 733 * <p>Type Error: the expression fails a static type check. Below are some examples. 734 * 735 * <ul> 736 * <li>"sin(2, 3)" - wrong number of arguments for the sin function 737 * <li>"this.childrenRankingSignals() + 1" - cannot add a list with a number 738 * <li>"this.propertyWeights()" - the final type of the overall expression cannot be a 739 * list, which can be fixed by "max(this.propertyWeights())" 740 * <li>"abs(this.propertyWeights())" - the abs function does not support list type 741 * arguments 742 * <li>"print(2)" - unknown function 743 * </ul> 744 * 745 * <p>Evaluation Error: an error occurred while evaluating the value of the expression. 746 * Below are some examples. 747 * 748 * <ul> 749 * <li>"1 / 0", "log(0)", "1 + sqrt(-1)" - getting a non-finite value in the middle of 750 * evaluation 751 * <li>"this.usageCount(1 + 0.5)" - expect the argument to be an integer. Note that this 752 * is not a type error and "this.usageCount(1.5 + 1/2)" can succeed without any issues 753 * <li>"this.documentScore()" - in case of an IO error, this will be an evaluation error 754 * </ul> 755 * 756 * <p>Syntax errors and type errors will fail the entire search and will cause {@link 757 * SearchResults#getNextPage} to throw an {@link AppSearchException} with the result code of 758 * {@link AppSearchResult#RESULT_INVALID_ARGUMENT}. 759 * 760 * <p>Evaluation errors will result in the offending documents receiving the default score. 761 * For {@link #ORDER_DESCENDING}, the default score will be 0, for {@link #ORDER_ASCENDING} 762 * the default score will be infinity. 763 * 764 * @param advancedRankingExpression a non-empty string representing the ranking expression. 765 */ 766 @CanIgnoreReturnValue 767 @NonNull setRankingStrategy(@onNull String advancedRankingExpression)768 public Builder setRankingStrategy(@NonNull String advancedRankingExpression) { 769 Preconditions.checkStringNotEmpty(advancedRankingExpression); 770 resetIfBuilt(); 771 mRankingStrategy = RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION; 772 mAdvancedRankingExpression = advancedRankingExpression; 773 return this; 774 } 775 776 /** 777 * Sets the order of returned search results, the default is {@link #ORDER_DESCENDING}, 778 * meaning that results with higher scores come first. 779 * 780 * <p>This order field will be ignored if RankingStrategy = {@code RANKING_STRATEGY_NONE}. 781 */ 782 @CanIgnoreReturnValue 783 @NonNull setOrder(@rder int order)784 public Builder setOrder(@Order int order) { 785 Preconditions.checkArgumentInRange( 786 order, ORDER_DESCENDING, ORDER_ASCENDING, "Result ranking order"); 787 resetIfBuilt(); 788 mOrder = order; 789 return this; 790 } 791 792 /** 793 * Sets the {@code snippetCount} such that the first {@code snippetCount} documents based on 794 * the ranking strategy will have snippet information provided. 795 * 796 * <p>The list returned from {@link SearchResult#getMatchInfos} will contain at most this 797 * many entries. 798 * 799 * <p>If set to 0 (default), snippeting is disabled and the list returned from {@link 800 * SearchResult#getMatchInfos} will be empty. 801 */ 802 @CanIgnoreReturnValue 803 @NonNull setSnippetCount( @ntRangefrom = 0, to = MAX_SNIPPET_COUNT) int snippetCount)804 public SearchSpec.Builder setSnippetCount( 805 @IntRange(from = 0, to = MAX_SNIPPET_COUNT) int snippetCount) { 806 Preconditions.checkArgumentInRange(snippetCount, 0, MAX_SNIPPET_COUNT, "snippetCount"); 807 resetIfBuilt(); 808 mSnippetCount = snippetCount; 809 return this; 810 } 811 812 /** 813 * Sets {@code snippetCountPerProperty}. Only the first {@code snippetCountPerProperty} 814 * snippets for each property of each {@link GenericDocument} will contain snippet 815 * information. 816 * 817 * <p>If set to 0, snippeting is disabled and the list returned from {@link 818 * SearchResult#getMatchInfos} will be empty. 819 * 820 * <p>The default behavior is to snippet all matches a property contains, up to the maximum 821 * value of 10,000. 822 */ 823 @CanIgnoreReturnValue 824 @NonNull setSnippetCountPerProperty( @ntRangefrom = 0, to = MAX_SNIPPET_PER_PROPERTY_COUNT) int snippetCountPerProperty)825 public SearchSpec.Builder setSnippetCountPerProperty( 826 @IntRange(from = 0, to = MAX_SNIPPET_PER_PROPERTY_COUNT) 827 int snippetCountPerProperty) { 828 Preconditions.checkArgumentInRange( 829 snippetCountPerProperty, 830 0, 831 MAX_SNIPPET_PER_PROPERTY_COUNT, 832 "snippetCountPerProperty"); 833 resetIfBuilt(); 834 mSnippetCountPerProperty = snippetCountPerProperty; 835 return this; 836 } 837 838 /** 839 * Sets {@code maxSnippetSize}, the maximum snippet size. Snippet windows start at {@code 840 * maxSnippetSize/2} bytes before the middle of the matching token and end at {@code 841 * maxSnippetSize/2} bytes after the middle of the matching token. It respects token 842 * boundaries, therefore the returned window may be smaller than requested. 843 * 844 * <p>Setting {@code maxSnippetSize} to 0 will disable windowing and an empty String will be 845 * returned. If matches enabled is also set to false, then snippeting is disabled. 846 * 847 * <p>For example, {@code maxSnippetSize} = 16. "foo bar baz bat rat" with a query of "baz" 848 * will return a window of "bar baz bat" which is only 11 bytes long. 849 */ 850 @CanIgnoreReturnValue 851 @NonNull setMaxSnippetSize( @ntRangefrom = 0, to = MAX_SNIPPET_SIZE_LIMIT) int maxSnippetSize)852 public SearchSpec.Builder setMaxSnippetSize( 853 @IntRange(from = 0, to = MAX_SNIPPET_SIZE_LIMIT) int maxSnippetSize) { 854 Preconditions.checkArgumentInRange( 855 maxSnippetSize, 0, MAX_SNIPPET_SIZE_LIMIT, "maxSnippetSize"); 856 resetIfBuilt(); 857 mMaxSnippetSize = maxSnippetSize; 858 return this; 859 } 860 861 /** 862 * Adds property paths for the specified type to be used for projection. If property paths 863 * are added for a type, then only the properties referred to will be retrieved for results 864 * of that type. If a property path that is specified isn't present in a result, it will be 865 * ignored for that result. Property paths cannot be null. 866 * 867 * @see #addProjectionPaths 868 * @param schema a string corresponding to the schema to add projections to. 869 * @param propertyPaths the projections to add. 870 */ 871 @CanIgnoreReturnValue 872 @NonNull addProjection( @onNull String schema, @NonNull Collection<String> propertyPaths)873 public SearchSpec.Builder addProjection( 874 @NonNull String schema, @NonNull Collection<String> propertyPaths) { 875 Objects.requireNonNull(schema); 876 Objects.requireNonNull(propertyPaths); 877 resetIfBuilt(); 878 ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size()); 879 for (String propertyPath : propertyPaths) { 880 Objects.requireNonNull(propertyPath); 881 propertyPathsArrayList.add(propertyPath); 882 } 883 mProjectionTypePropertyMasks.putStringArrayList(schema, propertyPathsArrayList); 884 return this; 885 } 886 887 /** 888 * Adds property paths for the specified type to be used for projection. If property paths 889 * are added for a type, then only the properties referred to will be retrieved for results 890 * of that type. If a property path that is specified isn't present in a result, it will be 891 * ignored for that result. Property paths cannot be null. 892 * 893 * <p>If no property paths are added for a particular type, then all properties of results 894 * of that type will be retrieved. 895 * 896 * <p>If property path is added for the {@link SearchSpec#PROJECTION_SCHEMA_TYPE_WILDCARD}, 897 * then those property paths will apply to all results, excepting any types that have their 898 * own, specific property paths set. 899 * 900 * <p>Suppose the following document is in the index. 901 * 902 * <pre>{@code 903 * Email: Document { 904 * sender: Document { 905 * name: "Mr. Person" 906 * email: "mrperson123@google.com" 907 * } 908 * recipients: [ 909 * Document { 910 * name: "John Doe" 911 * email: "johndoe123@google.com" 912 * } 913 * Document { 914 * name: "Jane Doe" 915 * email: "janedoe123@google.com" 916 * } 917 * ] 918 * subject: "IMPORTANT" 919 * body: "Limited time offer!" 920 * } 921 * }</pre> 922 * 923 * <p>Then, suppose that a query for "important" is issued with the following projection 924 * type property paths: 925 * 926 * <pre>{@code 927 * {schema: "Email", ["subject", "sender.name", "recipients.name"]} 928 * }</pre> 929 * 930 * <p>The above document will be returned as: 931 * 932 * <pre>{@code 933 * Email: Document { 934 * sender: Document { 935 * name: "Mr. Body" 936 * } 937 * recipients: [ 938 * Document { 939 * name: "John Doe" 940 * } 941 * Document { 942 * name: "Jane Doe" 943 * } 944 * ] 945 * subject: "IMPORTANT" 946 * } 947 * }</pre> 948 * 949 * @param schema a string corresponding to the schema to add projections to. 950 * @param propertyPaths the projections to add. 951 */ 952 @CanIgnoreReturnValue 953 @NonNull addProjectionPaths( @onNull String schema, @NonNull Collection<PropertyPath> propertyPaths)954 public SearchSpec.Builder addProjectionPaths( 955 @NonNull String schema, @NonNull Collection<PropertyPath> propertyPaths) { 956 Objects.requireNonNull(schema); 957 Objects.requireNonNull(propertyPaths); 958 ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size()); 959 for (PropertyPath propertyPath : propertyPaths) { 960 propertyPathsArrayList.add(propertyPath.toString()); 961 } 962 return addProjection(schema, propertyPathsArrayList); 963 } 964 965 /** 966 * Sets the maximum number of results to return for each group, where groups are defined by 967 * grouping type. 968 * 969 * <p>Calling this method will override any previous calls. So calling {@code 970 * setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 7)} and then calling {@code 971 * setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 2)} will result in only the latter, a limit 972 * of two results per package, being applied. Or calling {@code setResultGrouping 973 * (GROUPING_TYPE_PER_PACKAGE, 1)} and then calling {@code setResultGrouping 974 * (GROUPING_TYPE_PER_PACKAGE | GROUPING_PER_NAMESPACE, 5)} will result in five results per 975 * package per namespace. 976 * 977 * @param groupingTypeFlags One or more combination of grouping types. 978 * @param limit Number of results to return per {@code groupingTypeFlags}. 979 * @throws IllegalArgumentException if groupingTypeFlags is zero. 980 */ 981 // Individual parameters available from getResultGroupingTypeFlags and 982 // getResultGroupingLimit 983 @CanIgnoreReturnValue 984 @SuppressLint("MissingGetterMatchingBuilder") 985 @NonNull setResultGrouping(@roupingType int groupingTypeFlags, int limit)986 public Builder setResultGrouping(@GroupingType int groupingTypeFlags, int limit) { 987 Preconditions.checkState( 988 groupingTypeFlags != 0, "Result grouping type cannot be zero."); 989 resetIfBuilt(); 990 mGroupingTypeFlags = groupingTypeFlags; 991 mGroupingLimit = limit; 992 return this; 993 } 994 995 /** 996 * Sets property weights by schema type and property path. 997 * 998 * <p>Property weights are used to promote and demote query term matches within a {@link 999 * GenericDocument} property when applying scoring. 1000 * 1001 * <p>Property weights must be positive values (greater than 0). A property's weight is 1002 * multiplied with that property's scoring contribution. This means weights set between 0.0 1003 * and 1.0 demote scoring contributions by a term match within the property. Weights set 1004 * above 1.0 promote scoring contributions by a term match within the property. 1005 * 1006 * <p>Properties that exist in the {@link AppSearchSchema}, but do not have a weight 1007 * explicitly set will be given a default weight of 1.0. 1008 * 1009 * <p>Weights set for property paths that do not exist in the {@link AppSearchSchema} will 1010 * be discarded and not affect scoring. 1011 * 1012 * <p><b>NOTE:</b> Property weights only affect scoring for query-dependent scoring 1013 * strategies, such as {@link #RANKING_STRATEGY_RELEVANCE_SCORE}. 1014 * 1015 * @param schemaType the schema type to set property weights for. 1016 * @param propertyPathWeights a {@link Map} of property paths of the schema type to the 1017 * weight to set for that property. 1018 * @throws IllegalArgumentException if a weight is equal to or less than 0.0. 1019 */ 1020 @NonNull setPropertyWeights( @onNull String schemaType, @NonNull Map<String, Double> propertyPathWeights)1021 public SearchSpec.Builder setPropertyWeights( 1022 @NonNull String schemaType, @NonNull Map<String, Double> propertyPathWeights) { 1023 Objects.requireNonNull(schemaType); 1024 Objects.requireNonNull(propertyPathWeights); 1025 1026 Bundle propertyPathBundle = new Bundle(); 1027 for (Map.Entry<String, Double> propertyPathWeightEntry : 1028 propertyPathWeights.entrySet()) { 1029 String propertyPath = Objects.requireNonNull(propertyPathWeightEntry.getKey()); 1030 Double weight = Objects.requireNonNull(propertyPathWeightEntry.getValue()); 1031 if (weight <= 0.0) { 1032 throw new IllegalArgumentException( 1033 "Cannot set non-positive property weight " 1034 + "value " 1035 + weight 1036 + " for property path: " 1037 + propertyPath); 1038 } 1039 propertyPathBundle.putDouble(propertyPath, weight); 1040 } 1041 mTypePropertyWeights.putBundle(schemaType, propertyPathBundle); 1042 return this; 1043 } 1044 1045 /** 1046 * Specifies which documents to join with, and how to join. 1047 * 1048 * <p>If the ranking strategy is {@link #RANKING_STRATEGY_JOIN_AGGREGATE_SCORE}, and the 1049 * JoinSpec is null, {@link #build} will throw an {@link AppSearchException}. 1050 * 1051 * @param joinSpec a specification on how to perform the Join operation. 1052 */ 1053 @NonNull setJoinSpec(@onNull JoinSpec joinSpec)1054 public Builder setJoinSpec(@NonNull JoinSpec joinSpec) { 1055 resetIfBuilt(); 1056 mJoinSpec = Objects.requireNonNull(joinSpec); 1057 return this; 1058 } 1059 1060 /** 1061 * Sets property weights by schema type and property path. 1062 * 1063 * <p>Property weights are used to promote and demote query term matches within a {@link 1064 * GenericDocument} property when applying scoring. 1065 * 1066 * <p>Property weights must be positive values (greater than 0). A property's weight is 1067 * multiplied with that property's scoring contribution. This means weights set between 0.0 1068 * and 1.0 demote scoring contributions by a term match within the property. Weights set 1069 * above 1.0 promote scoring contributions by a term match within the property. 1070 * 1071 * <p>Properties that exist in the {@link AppSearchSchema}, but do not have a weight 1072 * explicitly set will be given a default weight of 1.0. 1073 * 1074 * <p>Weights set for property paths that do not exist in the {@link AppSearchSchema} will 1075 * be discarded and not affect scoring. 1076 * 1077 * <p><b>NOTE:</b> Property weights only affect scoring for query-dependent scoring 1078 * strategies, such as {@link #RANKING_STRATEGY_RELEVANCE_SCORE}. 1079 * 1080 * @param schemaType the schema type to set property weights for. 1081 * @param propertyPathWeights a {@link Map} of property paths of the schema type to the 1082 * weight to set for that property. 1083 * @throws IllegalArgumentException if a weight is equal to or less than 0.0. 1084 */ 1085 @NonNull setPropertyWeightPaths( @onNull String schemaType, @NonNull Map<PropertyPath, Double> propertyPathWeights)1086 public SearchSpec.Builder setPropertyWeightPaths( 1087 @NonNull String schemaType, 1088 @NonNull Map<PropertyPath, Double> propertyPathWeights) { 1089 Objects.requireNonNull(propertyPathWeights); 1090 1091 Map<String, Double> propertyWeights = new ArrayMap<>(propertyPathWeights.size()); 1092 for (Map.Entry<PropertyPath, Double> propertyPathWeightEntry : 1093 propertyPathWeights.entrySet()) { 1094 PropertyPath propertyPath = 1095 Objects.requireNonNull(propertyPathWeightEntry.getKey()); 1096 propertyWeights.put(propertyPath.toString(), propertyPathWeightEntry.getValue()); 1097 } 1098 return setPropertyWeights(schemaType, propertyWeights); 1099 } 1100 1101 /** 1102 * Sets the {@link Features#NUMERIC_SEARCH} feature as enabled/disabled according to the 1103 * enabled parameter. 1104 * 1105 * @param enabled Enables the feature if true, otherwise disables it. 1106 * <p>If disabled, disallows use of {@link 1107 * AppSearchSchema.LongPropertyConfig#INDEXING_TYPE_RANGE} and all other numeric 1108 * querying features. 1109 */ 1110 @NonNull setNumericSearchEnabled(boolean enabled)1111 public Builder setNumericSearchEnabled(boolean enabled) { 1112 modifyEnabledFeature(FeatureConstants.NUMERIC_SEARCH, enabled); 1113 return this; 1114 } 1115 1116 /** 1117 * Sets the {@link Features#VERBATIM_SEARCH} feature as enabled/disabled according to the 1118 * enabled parameter. 1119 * 1120 * @param enabled Enables the feature if true, otherwise disables it 1121 * <p>If disabled, disallows use of {@link 1122 * AppSearchSchema.StringPropertyConfig#TOKENIZER_TYPE_VERBATIM} and all other verbatim 1123 * search features within the query language that allows clients to search using the 1124 * verbatim string operator. 1125 * <p>For example, The verbatim string operator '"foo/bar" OR baz' will ensure that 1126 * 'foo/bar' is treated as a single 'verbatim' token. 1127 */ 1128 @NonNull setVerbatimSearchEnabled(boolean enabled)1129 public Builder setVerbatimSearchEnabled(boolean enabled) { 1130 modifyEnabledFeature(FeatureConstants.VERBATIM_SEARCH, enabled); 1131 return this; 1132 } 1133 1134 /** 1135 * Sets the {@link Features#LIST_FILTER_QUERY_LANGUAGE} feature as enabled/disabled 1136 * according to the enabled parameter. 1137 * 1138 * @param enabled Enables the feature if true, otherwise disables it. 1139 * <p>This feature covers the expansion of the query language to conform to the 1140 * definition of the list filters language (https://aip.dev/160). This includes: 1141 * <ul> 1142 * <li>addition of explicit 'AND' and 'NOT' operators 1143 * <li>property restricts are allowed with grouping (ex. "prop:(a OR b)") 1144 * <li>addition of custom functions to control matching 1145 * </ul> 1146 * <p>The newly added custom functions covered by this feature are: 1147 * <ul> 1148 * <li>createList(String...) 1149 * <li>termSearch(String, List<String>) 1150 * </ul> 1151 * <p>createList takes a variable number of strings and returns a list of strings. It is 1152 * for use with termSearch. 1153 * <p>termSearch takes a query string that will be parsed according to the supported 1154 * query language and an optional list of strings that specify the properties to be 1155 * restricted to. This exists as a convenience for multiple property restricts. So, for 1156 * example, the query "(subject:foo OR body:foo) (subject:bar OR body:bar)" could be 1157 * rewritten as "termSearch(\"foo bar\", createList(\"subject\", \"bar\"))" 1158 */ 1159 @NonNull setListFilterQueryLanguageEnabled(boolean enabled)1160 public Builder setListFilterQueryLanguageEnabled(boolean enabled) { 1161 modifyEnabledFeature(FeatureConstants.LIST_FILTER_QUERY_LANGUAGE, enabled); 1162 return this; 1163 } 1164 1165 /** 1166 * Constructs a new {@link SearchSpec} from the contents of this builder. 1167 * 1168 * @throws IllegalArgumentException if property weights are provided with a ranking strategy 1169 * that isn't RANKING_STRATEGY_RELEVANCE_SCORE. 1170 * @throws IllegalStateException if the ranking strategy is {@link 1171 * #RANKING_STRATEGY_JOIN_AGGREGATE_SCORE} and {@link #setJoinSpec} has never been 1172 * called. 1173 * @throws IllegalStateException if the aggregation scoring strategy has been set in {@link 1174 * JoinSpec#getAggregationScoringStrategy()} but the ranking strategy is not {@link 1175 * #RANKING_STRATEGY_JOIN_AGGREGATE_SCORE}. 1176 */ 1177 @NonNull build()1178 public SearchSpec build() { 1179 Bundle bundle = new Bundle(); 1180 if (mJoinSpec != null) { 1181 if (mRankingStrategy != RANKING_STRATEGY_JOIN_AGGREGATE_SCORE 1182 && mJoinSpec.getAggregationScoringStrategy() 1183 != JoinSpec.AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL) { 1184 throw new IllegalStateException( 1185 "Aggregate scoring strategy has been set in " 1186 + "the nested JoinSpec, but ranking strategy is not " 1187 + "RANKING_STRATEGY_JOIN_AGGREGATE_SCORE"); 1188 } 1189 bundle.putBundle(JOIN_SPEC, mJoinSpec.getBundle()); 1190 } else if (mRankingStrategy == RANKING_STRATEGY_JOIN_AGGREGATE_SCORE) { 1191 throw new IllegalStateException( 1192 "Attempting to rank based on joined documents, but " 1193 + "no JoinSpec provided"); 1194 } 1195 bundle.putStringArrayList(SCHEMA_FIELD, mSchemas); 1196 bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces); 1197 bundle.putStringArrayList(PACKAGE_NAME_FIELD, mPackageNames); 1198 bundle.putStringArrayList(ENABLED_FEATURES_FIELD, new ArrayList<>(mEnabledFeatures)); 1199 bundle.putBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD, mProjectionTypePropertyMasks); 1200 bundle.putInt(NUM_PER_PAGE_FIELD, mResultCountPerPage); 1201 bundle.putInt(TERM_MATCH_TYPE_FIELD, mTermMatchType); 1202 bundle.putInt(SNIPPET_COUNT_FIELD, mSnippetCount); 1203 bundle.putInt(SNIPPET_COUNT_PER_PROPERTY_FIELD, mSnippetCountPerProperty); 1204 bundle.putInt(MAX_SNIPPET_FIELD, mMaxSnippetSize); 1205 bundle.putInt(RANKING_STRATEGY_FIELD, mRankingStrategy); 1206 bundle.putInt(ORDER_FIELD, mOrder); 1207 bundle.putInt(RESULT_GROUPING_TYPE_FLAGS, mGroupingTypeFlags); 1208 bundle.putInt(RESULT_GROUPING_LIMIT, mGroupingLimit); 1209 if (!mTypePropertyWeights.isEmpty() 1210 && RANKING_STRATEGY_RELEVANCE_SCORE != mRankingStrategy 1211 && RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION != mRankingStrategy) { 1212 throw new IllegalArgumentException( 1213 "Property weights are only compatible with the" 1214 + " RANKING_STRATEGY_RELEVANCE_SCORE and" 1215 + " RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION ranking strategies."); 1216 } 1217 bundle.putBundle(TYPE_PROPERTY_WEIGHTS_FIELD, mTypePropertyWeights); 1218 bundle.putString(ADVANCED_RANKING_EXPRESSION, mAdvancedRankingExpression); 1219 mBuilt = true; 1220 return new SearchSpec(bundle); 1221 } 1222 resetIfBuilt()1223 private void resetIfBuilt() { 1224 if (mBuilt) { 1225 mSchemas = new ArrayList<>(mSchemas); 1226 mNamespaces = new ArrayList<>(mNamespaces); 1227 mPackageNames = new ArrayList<>(mPackageNames); 1228 mProjectionTypePropertyMasks = BundleUtil.deepCopy(mProjectionTypePropertyMasks); 1229 mTypePropertyWeights = BundleUtil.deepCopy(mTypePropertyWeights); 1230 mBuilt = false; 1231 } 1232 } 1233 modifyEnabledFeature(@onNull String feature, boolean enabled)1234 private void modifyEnabledFeature(@NonNull String feature, boolean enabled) { 1235 resetIfBuilt(); 1236 if (enabled) { 1237 mEnabledFeatures.add(feature); 1238 } else { 1239 mEnabledFeatures.remove(feature); 1240 } 1241 } 1242 } 1243 } 1244