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