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