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