• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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