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