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 com.android.server.appsearch.external.localstorage.converter;
18 
19 import android.annotation.NonNull;
20 import android.app.appsearch.SearchSuggestionSpec;
21 
22 import com.android.server.appsearch.external.localstorage.SchemaCache;
23 
24 import com.google.android.icing.proto.NamespaceDocumentUriGroup;
25 import com.google.android.icing.proto.SuggestionScoringSpecProto;
26 import com.google.android.icing.proto.SuggestionSpecProto;
27 import com.google.android.icing.proto.TermMatchType;
28 import com.google.android.icing.proto.TypePropertyMask;
29 
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Objects;
33 import java.util.Set;
34 
35 /**
36  * Translates a {@link SearchSuggestionSpec} into icing search protos.
37  *
38  * @hide
39  */
40 public final class SearchSuggestionSpecToProtoConverter {
41     private final String mSuggestionQueryExpression;
42     private final SearchSuggestionSpec mSearchSuggestionSpec;
43 
44     /**
45      * The client specific packages and databases to search for. For local storage, this always
46      * contains a single prefix.
47      */
48     private final Set<String> mPrefixes;
49 
50     /**
51      * The intersected prefixed namespaces that are existing in AppSearch and also accessible to the
52      * client.
53      */
54     private final Set<String> mTargetPrefixedNamespaceFilters;
55 
56     /**
57      * The intersected prefixed schema types that are existing in AppSearch and also accessible to
58      * the client.
59      */
60     private final Set<String> mTargetPrefixedSchemaFilters;
61 
62     /**
63      * Creates a {@link SearchSuggestionSpecToProtoConverter} for given {@link
64      * SearchSuggestionSpec}.
65      *
66      * @param suggestionQueryExpression The non-empty query expression used to be completed.
67      * @param searchSuggestionSpec The spec we need to convert from.
68      * @param prefixes Set of database prefix which the caller want to access.
69      * @param namespaceMap The cached Map of {@code <Prefix, Set<PrefixedNamespace>>} stores all
70      *     prefixed namespace filters which are stored in AppSearch.
71      */
SearchSuggestionSpecToProtoConverter( @onNull String suggestionQueryExpression, @NonNull SearchSuggestionSpec searchSuggestionSpec, @NonNull Set<String> prefixes, @NonNull Map<String, Set<String>> namespaceMap, @NonNull SchemaCache schemaCache)72     public SearchSuggestionSpecToProtoConverter(
73             @NonNull String suggestionQueryExpression,
74             @NonNull SearchSuggestionSpec searchSuggestionSpec,
75             @NonNull Set<String> prefixes,
76             @NonNull Map<String, Set<String>> namespaceMap,
77             @NonNull SchemaCache schemaCache) {
78         mSuggestionQueryExpression = Objects.requireNonNull(suggestionQueryExpression);
79         mSearchSuggestionSpec = Objects.requireNonNull(searchSuggestionSpec);
80         mPrefixes = Objects.requireNonNull(prefixes);
81         Objects.requireNonNull(namespaceMap);
82         mTargetPrefixedNamespaceFilters =
83                 SearchSpecToProtoConverterUtil.generateTargetNamespaceFilters(
84                         prefixes, namespaceMap, searchSuggestionSpec.getFilterNamespaces());
85         mTargetPrefixedSchemaFilters =
86                 SearchSpecToProtoConverterUtil.generateTargetSchemaFilters(
87                         prefixes, schemaCache, searchSuggestionSpec.getFilterSchemas());
88     }
89 
90     /**
91      * Returns whether this search's target filters are empty. If any target filter is empty, we
92      * should skip send request to Icing.
93      */
hasNothingToSearch()94     public boolean hasNothingToSearch() {
95         return mTargetPrefixedNamespaceFilters.isEmpty() || mTargetPrefixedSchemaFilters.isEmpty();
96     }
97 
98     /** Extracts {@link SuggestionSpecProto} information from a {@link SearchSuggestionSpec}. */
99     @NonNull
toSearchSuggestionSpecProto()100     public SuggestionSpecProto toSearchSuggestionSpecProto() {
101         // Set query suggestion prefix to the SearchSuggestionProto and override schema and
102         // namespace filter by targetPrefixedFilters which contains all existing and also
103         // accessible to the caller filters.
104         SuggestionSpecProto.Builder protoBuilder =
105                 SuggestionSpecProto.newBuilder()
106                         .setPrefix(mSuggestionQueryExpression)
107                         .addAllNamespaceFilters(mTargetPrefixedNamespaceFilters)
108                         .addAllSchemaTypeFilters(mTargetPrefixedSchemaFilters)
109                         .setNumToReturn(mSearchSuggestionSpec.getMaximumResultCount());
110 
111         // Convert type property filter map into type property mask proto.
112         for (Map.Entry<String, List<String>> entry :
113                 mSearchSuggestionSpec.getFilterProperties().entrySet()) {
114             for (String prefix : mPrefixes) {
115                 String prefixedSchemaType = prefix + entry.getKey();
116                 if (mTargetPrefixedSchemaFilters.contains(prefixedSchemaType)) {
117                     protoBuilder.addTypePropertyFilters(
118                             TypePropertyMask.newBuilder()
119                                     .setSchemaType(prefixedSchemaType)
120                                     .addAllPaths(entry.getValue())
121                                     .build());
122                 }
123             }
124         }
125 
126         // Convert the document ids filters
127         for (Map.Entry<String, List<String>> entry :
128                 mSearchSuggestionSpec.getFilterDocumentIds().entrySet()) {
129             for (String prefix : mPrefixes) {
130                 String prefixedNamespace = prefix + entry.getKey();
131                 if (mTargetPrefixedNamespaceFilters.contains(prefixedNamespace)) {
132                     protoBuilder.addDocumentUriFilters(
133                             NamespaceDocumentUriGroup.newBuilder()
134                                     .setNamespace(prefixedNamespace)
135                                     .addAllDocumentUris(entry.getValue())
136                                     .build());
137                 }
138             }
139         }
140 
141         // TODO(b/227356108) expose setTermMatch in SearchSuggestionSpec.
142         protoBuilder.setScoringSpec(
143                 SuggestionScoringSpecProto.newBuilder()
144                         .setScoringMatchType(TermMatchType.Code.EXACT_ONLY)
145                         .setRankBy(
146                                 toProtoRankingStrategy(mSearchSuggestionSpec.getRankingStrategy()))
147                         .build());
148 
149         return protoBuilder.build();
150     }
151 
toProtoRankingStrategy( @earchSuggestionSpec.SuggestionRankingStrategy int rankingStrategyCode)152     private static SuggestionScoringSpecProto.SuggestionRankingStrategy.Code toProtoRankingStrategy(
153             @SearchSuggestionSpec.SuggestionRankingStrategy int rankingStrategyCode) {
154         switch (rankingStrategyCode) {
155             case SearchSuggestionSpec.SUGGESTION_RANKING_STRATEGY_NONE:
156                 return SuggestionScoringSpecProto.SuggestionRankingStrategy.Code.NONE;
157             case SearchSuggestionSpec.SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT:
158                 return SuggestionScoringSpecProto.SuggestionRankingStrategy.Code.DOCUMENT_COUNT;
159             case SearchSuggestionSpec.SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY:
160                 return SuggestionScoringSpecProto.SuggestionRankingStrategy.Code.TERM_FREQUENCY;
161             default:
162                 throw new IllegalArgumentException(
163                         "Invalid suggestion ranking strategy: " + rankingStrategyCode);
164         }
165     }
166 }
167