1 /* 2 * Copyright 2020 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 androidx.appsearch.localstorage.converter; 18 19 import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix; 20 import static androidx.appsearch.localstorage.util.PrefixUtil.getPackageName; 21 import static androidx.appsearch.localstorage.util.PrefixUtil.getPrefix; 22 import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefix; 23 24 import android.util.Log; 25 26 import androidx.annotation.OptIn; 27 import androidx.annotation.RestrictTo; 28 import androidx.appsearch.app.EmbeddingVector; 29 import androidx.appsearch.app.ExperimentalAppSearchApi; 30 import androidx.appsearch.app.FeatureConstants; 31 import androidx.appsearch.app.JoinSpec; 32 import androidx.appsearch.app.SearchResult; 33 import androidx.appsearch.app.SearchSpec; 34 import androidx.appsearch.exceptions.AppSearchException; 35 import androidx.appsearch.localstorage.IcingOptionsConfig; 36 import androidx.appsearch.localstorage.NamespaceCache; 37 import androidx.appsearch.localstorage.SchemaCache; 38 import androidx.appsearch.localstorage.visibilitystore.CallerAccess; 39 import androidx.appsearch.localstorage.visibilitystore.VisibilityChecker; 40 import androidx.appsearch.localstorage.visibilitystore.VisibilityStore; 41 import androidx.appsearch.localstorage.visibilitystore.VisibilityUtil; 42 import androidx.collection.ArrayMap; 43 import androidx.collection.ArraySet; 44 import androidx.core.util.Preconditions; 45 46 import com.google.android.icing.proto.JoinSpecProto; 47 import com.google.android.icing.proto.NamespaceDocumentUriGroup; 48 import com.google.android.icing.proto.PropertyWeight; 49 import com.google.android.icing.proto.ResultSpecProto; 50 import com.google.android.icing.proto.SchemaTypeAliasMapProto; 51 import com.google.android.icing.proto.SchemaTypeConfigProto; 52 import com.google.android.icing.proto.ScoringFeatureType; 53 import com.google.android.icing.proto.ScoringSpecProto; 54 import com.google.android.icing.proto.SearchSpecProto; 55 import com.google.android.icing.proto.TermMatchType; 56 import com.google.android.icing.proto.TypePropertyMask; 57 import com.google.android.icing.proto.TypePropertyWeights; 58 59 import org.jspecify.annotations.NonNull; 60 import org.jspecify.annotations.Nullable; 61 62 import java.util.ArrayList; 63 import java.util.Iterator; 64 import java.util.List; 65 import java.util.Map; 66 import java.util.Set; 67 68 /** 69 * Translates a {@link SearchSpec} into icing search protos. 70 * 71 * @exportToFramework:hide 72 */ 73 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 74 public final class SearchSpecToProtoConverter { 75 private static final String TAG = "AppSearchSearchSpecConv"; 76 private final String mQueryExpression; 77 private final SearchSpec mSearchSpec; 78 /** 79 * The union of allowed prefixes for the top-level SearchSpec and any nested SearchSpecs. 80 */ 81 private final Set<String> mAllAllowedPrefixes; 82 /** 83 * The intersection of mAllAllowedPrefixes and prefixes requested in the SearchSpec currently 84 * being handled. 85 */ 86 private final Set<String> mCurrentSearchSpecPrefixFilters; 87 /** 88 * The intersected prefixed namespaces that are existing in AppSearch and also accessible to the 89 * client. 90 */ 91 private final Set<String> mTargetPrefixedNamespaceFilters; 92 /** 93 * The intersected prefixed schema types that are existing in AppSearch and also accessible to 94 * the client. 95 */ 96 private final Set<String> mTargetPrefixedSchemaFilters; 97 98 /** 99 * The NamespaceCache instance held in AppSearch. 100 */ 101 private final NamespaceCache mNamespaceCache; 102 103 /** 104 * The SchemaCache instance held in AppSearch. 105 */ 106 private final SchemaCache mSchemaCache; 107 108 /** 109 * Optional config flags in {@link SearchSpecProto}. 110 */ 111 private final IcingOptionsConfig mIcingOptionsConfig; 112 113 /** 114 * The nested converter, which contains SearchSpec, ResultSpec, and ScoringSpec information 115 * about the nested query. This will remain null if there is no nested {@link JoinSpec}. 116 */ 117 private @Nullable SearchSpecToProtoConverter mNestedConverter = null; 118 119 /** 120 * Creates a {@link SearchSpecToProtoConverter} for given {@link SearchSpec}. 121 * 122 * @param queryExpression Query String to search. 123 * @param searchSpec The spec we need to convert from. 124 * @param allAllowedPrefixes Superset of database prefixes which the {@link SearchSpec} and all 125 * nested SearchSpecs are allowed to access. An empty set means no 126 * database prefixes are allowed, so nothing will be searched. 127 * @param namespaceCache The NamespaceCache instance held in AppSearch. 128 * @param schemaCache The SchemaCache instance held in AppSearch. 129 */ SearchSpecToProtoConverter( @onNull String queryExpression, @NonNull SearchSpec searchSpec, @NonNull Set<String> allAllowedPrefixes, @NonNull NamespaceCache namespaceCache, @NonNull SchemaCache schemaCache, @NonNull IcingOptionsConfig icingOptionsConfig)130 public SearchSpecToProtoConverter( 131 @NonNull String queryExpression, 132 @NonNull SearchSpec searchSpec, 133 @NonNull Set<String> allAllowedPrefixes, 134 @NonNull NamespaceCache namespaceCache, 135 @NonNull SchemaCache schemaCache, 136 @NonNull IcingOptionsConfig icingOptionsConfig) { 137 mQueryExpression = Preconditions.checkNotNull(queryExpression); 138 mSearchSpec = Preconditions.checkNotNull(searchSpec); 139 mAllAllowedPrefixes = Preconditions.checkNotNull(allAllowedPrefixes); 140 mNamespaceCache = Preconditions.checkNotNull(namespaceCache); 141 mSchemaCache = Preconditions.checkNotNull(schemaCache); 142 mIcingOptionsConfig = Preconditions.checkNotNull(icingOptionsConfig); 143 144 // This field holds the prefix filters for the SearchSpec currently being handled, which 145 // could be an outer or inner SearchSpec. If this constructor is called from outside of 146 // SearchSpecToProtoConverter, it will be handling an outer SearchSpec. If this SearchSpec 147 // contains a JoinSpec, the nested SearchSpec will be handled in the creation of 148 // mNestedConverter. This is useful as the two SearchSpecs could have different package 149 // filters. 150 List<String> packageFilters = searchSpec.getFilterPackageNames(); 151 if (packageFilters.isEmpty()) { 152 mCurrentSearchSpecPrefixFilters = mAllAllowedPrefixes; 153 } else { 154 mCurrentSearchSpecPrefixFilters = new ArraySet<>(); 155 for (String prefix : mAllAllowedPrefixes) { 156 String packageName = getPackageName(prefix); 157 if (packageFilters.contains(packageName)) { 158 // This performs an intersection of allowedPrefixes with prefixes requested 159 // in the SearchSpec currently being handled. The same operation is done 160 // on the nested SearchSpecs when mNestedConverter is created. 161 mCurrentSearchSpecPrefixFilters.add(prefix); 162 } 163 } 164 } 165 166 mTargetPrefixedNamespaceFilters = 167 SearchSpecToProtoConverterUtil.generateTargetNamespaceFilters( 168 mCurrentSearchSpecPrefixFilters, namespaceCache, 169 searchSpec.getFilterNamespaces()); 170 171 // If the target namespace filter is empty, the user has nothing to search for. We can skip 172 // generate the target schema filter. 173 if (!mTargetPrefixedNamespaceFilters.isEmpty()) { 174 mTargetPrefixedSchemaFilters = 175 SearchSpecToProtoConverterUtil.generateTargetSchemaFilters( 176 mCurrentSearchSpecPrefixFilters, schemaCache, 177 searchSpec.getFilterSchemas()); 178 } else { 179 mTargetPrefixedSchemaFilters = new ArraySet<>(); 180 } 181 182 JoinSpec joinSpec = searchSpec.getJoinSpec(); 183 if (joinSpec == null) { 184 return; 185 } 186 187 mNestedConverter = new SearchSpecToProtoConverter( 188 joinSpec.getNestedQuery(), 189 joinSpec.getNestedSearchSpec(), 190 mAllAllowedPrefixes, 191 namespaceCache, 192 schemaCache, 193 mIcingOptionsConfig); 194 } 195 196 /** 197 * Returns whether this search's target filters are empty. If any target filter is empty, we 198 * should skip send request to Icing. 199 * 200 * <p>The nestedConverter is not checked as {@link SearchResult}s from the nested query have to 201 * be joined to a {@link SearchResult} from the parent query. If the parent query has nothing to 202 * search, then so does the child query. 203 */ hasNothingToSearch()204 public boolean hasNothingToSearch() { 205 return mTargetPrefixedNamespaceFilters.isEmpty() || mTargetPrefixedSchemaFilters.isEmpty(); 206 } 207 208 /** 209 * For each target schema, we will check visibility store is that accessible to the caller. And 210 * remove this schemas if it is not allowed for caller to query. 211 * 212 * @param callerAccess Visibility access info of the calling app 213 * @param visibilityStore The {@link VisibilityStore} that store all visibility 214 * information. 215 * @param visibilityChecker Optional visibility checker to check whether the caller 216 * could access target schemas. Pass {@code null} will 217 * reject access for all documents which doesn't belong 218 * to the calling package. 219 */ removeInaccessibleSchemaFilter( @onNull CallerAccess callerAccess, @Nullable VisibilityStore visibilityStore, @Nullable VisibilityChecker visibilityChecker)220 public void removeInaccessibleSchemaFilter( 221 @NonNull CallerAccess callerAccess, 222 @Nullable VisibilityStore visibilityStore, 223 @Nullable VisibilityChecker visibilityChecker) { 224 removeInaccessibleSchemaFilterCached(callerAccess, visibilityStore, 225 /*inaccessibleSchemaPrefixes=*/new ArraySet<>(), 226 /*accessibleSchemaPrefixes=*/new ArraySet<>(), visibilityChecker); 227 } 228 229 /** 230 * For each target schema, we will check visibility store is that accessible to the caller. And 231 * remove this schemas if it is not allowed for caller to query. This private version accepts 232 * two additional parameters to minimize the amount of calls to 233 * {@link VisibilityUtil#isSchemaSearchableByCaller}. 234 * 235 * @param callerAccess Visibility access info of the calling app 236 * @param visibilityStore The {@link VisibilityStore} that store all visibility 237 * information. 238 * @param visibilityChecker Optional visibility checker to check whether the caller 239 * could access target schemas. Pass {@code null} will 240 * reject access for all documents which doesn't belong 241 * to the calling package. 242 * @param inaccessibleSchemaPrefixes A set of schemas that are known to be inaccessible. This 243 * is helpful for reducing duplicate calls to 244 * {@link VisibilityUtil}. 245 * @param accessibleSchemaPrefixes A set of schemas that are known to be accessible. This is 246 * helpful for reducing duplicate calls to 247 * {@link VisibilityUtil}. 248 */ removeInaccessibleSchemaFilterCached( @onNull CallerAccess callerAccess, @Nullable VisibilityStore visibilityStore, @NonNull Set<String> inaccessibleSchemaPrefixes, @NonNull Set<String> accessibleSchemaPrefixes, @Nullable VisibilityChecker visibilityChecker)249 private void removeInaccessibleSchemaFilterCached( 250 @NonNull CallerAccess callerAccess, 251 @Nullable VisibilityStore visibilityStore, 252 @NonNull Set<String> inaccessibleSchemaPrefixes, 253 @NonNull Set<String> accessibleSchemaPrefixes, 254 @Nullable VisibilityChecker visibilityChecker) { 255 Iterator<String> targetPrefixedSchemaFilterIterator = 256 mTargetPrefixedSchemaFilters.iterator(); 257 while (targetPrefixedSchemaFilterIterator.hasNext()) { 258 String targetPrefixedSchemaFilter = targetPrefixedSchemaFilterIterator.next(); 259 String packageName = getPackageName(targetPrefixedSchemaFilter); 260 261 if (accessibleSchemaPrefixes.contains(targetPrefixedSchemaFilter)) { 262 continue; 263 } else if (inaccessibleSchemaPrefixes.contains(targetPrefixedSchemaFilter)) { 264 targetPrefixedSchemaFilterIterator.remove(); 265 } else if (!VisibilityUtil.isSchemaSearchableByCaller( 266 callerAccess, 267 packageName, 268 targetPrefixedSchemaFilter, 269 visibilityStore, 270 visibilityChecker)) { 271 targetPrefixedSchemaFilterIterator.remove(); 272 inaccessibleSchemaPrefixes.add(targetPrefixedSchemaFilter); 273 } else { 274 accessibleSchemaPrefixes.add(targetPrefixedSchemaFilter); 275 } 276 } 277 278 if (mNestedConverter != null) { 279 mNestedConverter.removeInaccessibleSchemaFilterCached( 280 callerAccess, visibilityStore, inaccessibleSchemaPrefixes, 281 accessibleSchemaPrefixes, visibilityChecker); 282 } 283 } 284 285 286 /** Extracts {@link SearchSpecProto} information from a {@link SearchSpec}. */ 287 @OptIn(markerClass = ExperimentalAppSearchApi.class) toSearchSpecProto()288 public @NonNull SearchSpecProto toSearchSpecProto() { 289 // set query to SearchSpecProto and override schema and namespace filter by 290 // targetPrefixedFilters which contains all existing and also accessible to the caller 291 // filters. 292 SearchSpecProto.Builder protoBuilder = SearchSpecProto.newBuilder() 293 .setQuery(mQueryExpression) 294 .addAllNamespaceFilters(mTargetPrefixedNamespaceFilters) 295 .addAllSchemaTypeFilters(mTargetPrefixedSchemaFilters) 296 .setUseReadOnlySearch(mIcingOptionsConfig.getUseReadOnlySearch()) 297 .addAllQueryParameterStrings(mSearchSpec.getSearchStringParameters()); 298 299 List<EmbeddingVector> searchEmbeddings = mSearchSpec.getEmbeddingParameters(); 300 for (int i = 0; i < searchEmbeddings.size(); i++) { 301 protoBuilder.addEmbeddingQueryVectors( 302 GenericDocumentToProtoConverter.embeddingVectorToVectorProto( 303 searchEmbeddings.get(i))); 304 } 305 306 // Convert type property filter map into type property mask proto. 307 for (Map.Entry<String, List<String>> entry : 308 mSearchSpec.getFilterProperties().entrySet()) { 309 if (entry.getKey().equals(SearchSpec.SCHEMA_TYPE_WILDCARD)) { 310 protoBuilder.addTypePropertyFilters(TypePropertyMask.newBuilder() 311 .setSchemaType(SearchSpec.SCHEMA_TYPE_WILDCARD) 312 .addAllPaths(entry.getValue()) 313 .build()); 314 } else { 315 for (String prefix : mCurrentSearchSpecPrefixFilters) { 316 String prefixedSchemaType = prefix + entry.getKey(); 317 if (mTargetPrefixedSchemaFilters.contains(prefixedSchemaType)) { 318 protoBuilder.addTypePropertyFilters(TypePropertyMask.newBuilder() 319 .setSchemaType(prefixedSchemaType) 320 .addAllPaths(entry.getValue()) 321 .build()); 322 } 323 } 324 } 325 } 326 327 // Convert document id filters. 328 List<String> filterDocumentIds = mSearchSpec.getFilterDocumentIds(); 329 if (!filterDocumentIds.isEmpty()) { 330 for (String targetPrefixedNamespaceFilter : mTargetPrefixedNamespaceFilters) { 331 protoBuilder.addDocumentUriFilters(NamespaceDocumentUriGroup.newBuilder() 332 .setNamespace(targetPrefixedNamespaceFilter) 333 .addAllDocumentUris(filterDocumentIds) 334 .build()); 335 } 336 } 337 338 @SearchSpec.TermMatch int termMatchCode = mSearchSpec.getTermMatch(); 339 TermMatchType.Code termMatchCodeProto = TermMatchType.Code.forNumber(termMatchCode); 340 if (termMatchCodeProto == null || termMatchCodeProto.equals(TermMatchType.Code.UNKNOWN)) { 341 throw new IllegalArgumentException("Invalid term match type: " + termMatchCode); 342 } 343 protoBuilder.setTermMatchType(termMatchCodeProto); 344 345 @SearchSpec.EmbeddingSearchMetricType int embeddingSearchMetricType = 346 mSearchSpec.getDefaultEmbeddingSearchMetricType(); 347 SearchSpecProto.EmbeddingQueryMetricType.Code embeddingSearchMetricTypeProto = 348 SearchSpecProto.EmbeddingQueryMetricType.Code.forNumber(embeddingSearchMetricType); 349 if (embeddingSearchMetricTypeProto == null || embeddingSearchMetricTypeProto.equals( 350 SearchSpecProto.EmbeddingQueryMetricType.Code.UNKNOWN)) { 351 throw new IllegalArgumentException( 352 "Invalid embedding search metric type: " + embeddingSearchMetricType); 353 } 354 protoBuilder.setEmbeddingQueryMetricType(embeddingSearchMetricTypeProto); 355 356 if (mNestedConverter != null && !mNestedConverter.hasNothingToSearch()) { 357 JoinSpecProto.NestedSpecProto nestedSpec = 358 JoinSpecProto.NestedSpecProto.newBuilder() 359 .setResultSpec(mNestedConverter.toResultSpecProto( 360 mNamespaceCache, mSchemaCache)) 361 .setScoringSpec(mNestedConverter.toScoringSpecProto()) 362 .setSearchSpec(mNestedConverter.toSearchSpecProto()) 363 .build(); 364 365 // This cannot be null, otherwise mNestedConverter would be null as well. 366 JoinSpec joinSpec = mSearchSpec.getJoinSpec(); 367 JoinSpecProto.Builder joinSpecProtoBuilder = 368 JoinSpecProto.newBuilder() 369 .setNestedSpec(nestedSpec) 370 .setParentPropertyExpression(JoinSpec.QUALIFIED_ID) 371 .setChildPropertyExpression(joinSpec.getChildPropertyExpression()) 372 .setAggregationScoringStrategy( 373 toAggregationScoringStrategy( 374 joinSpec.getAggregationScoringStrategy())); 375 376 protoBuilder.setJoinSpec(joinSpecProtoBuilder); 377 } 378 379 if (mSearchSpec.isListFilterHasPropertyFunctionEnabled() 380 && !mIcingOptionsConfig.getBuildPropertyExistenceMetadataHits()) { 381 // This condition should never be reached as long as Features.isFeatureSupported() is 382 // consistent with IcingOptionsConfig. 383 throw new UnsupportedOperationException( 384 FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION 385 + " is currently not operational because the building process for the " 386 + "associated metadata has not yet been turned on."); 387 } 388 389 // Set enabled search features. 390 protoBuilder.addAllEnabledFeatures(toIcingSearchFeatures( 391 extractEnabledSearchFeatures(mSearchSpec.getEnabledFeatures()))); 392 393 return protoBuilder.build(); 394 } 395 396 /** 397 * Helper to convert to JoinSpecProto.AggregationScore. 398 * 399 * <p> {@link JoinSpec#AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL} will be treated as 400 * undefined, which is the default behavior. 401 * 402 * @param aggregationScoringStrategy the scoring strategy to convert. 403 */ 404 public static JoinSpecProto.AggregationScoringStrategy.@NonNull Code toAggregationScoringStrategy( @oinSpec.AggregationScoringStrategy int aggregationScoringStrategy)405 toAggregationScoringStrategy( 406 @JoinSpec.AggregationScoringStrategy int aggregationScoringStrategy) { 407 switch (aggregationScoringStrategy) { 408 case JoinSpec.AGGREGATION_SCORING_AVG_RANKING_SIGNAL: 409 return JoinSpecProto.AggregationScoringStrategy.Code.AVG; 410 case JoinSpec.AGGREGATION_SCORING_MIN_RANKING_SIGNAL: 411 return JoinSpecProto.AggregationScoringStrategy.Code.MIN; 412 case JoinSpec.AGGREGATION_SCORING_MAX_RANKING_SIGNAL: 413 return JoinSpecProto.AggregationScoringStrategy.Code.MAX; 414 case JoinSpec.AGGREGATION_SCORING_SUM_RANKING_SIGNAL: 415 return JoinSpecProto.AggregationScoringStrategy.Code.SUM; 416 case JoinSpec.AGGREGATION_SCORING_RESULT_COUNT: 417 return JoinSpecProto.AggregationScoringStrategy.Code.COUNT; 418 default: 419 return JoinSpecProto.AggregationScoringStrategy.Code.NONE; 420 } 421 } 422 423 /** 424 * Extracts {@link ResultSpecProto} information from a {@link SearchSpec}. 425 * 426 * @param namespaceCache The NamespaceCache instance held in AppSearch. 427 * @param schemaCache The SchemaCache instance held in AppSearch. 428 */ 429 @OptIn(markerClass = ExperimentalAppSearchApi.class) toResultSpecProto( @onNull NamespaceCache namespaceCache, @NonNull SchemaCache schemaCache)430 public @NonNull ResultSpecProto toResultSpecProto( 431 @NonNull NamespaceCache namespaceCache, 432 @NonNull SchemaCache schemaCache) { 433 ResultSpecProto.Builder resultSpecBuilder = ResultSpecProto.newBuilder() 434 .setNumPerPage(mSearchSpec.getResultCountPerPage()) 435 .setSnippetSpec( 436 ResultSpecProto.SnippetSpecProto.newBuilder() 437 .setNumToSnippet(mSearchSpec.getSnippetCount()) 438 .setNumMatchesPerProperty(mSearchSpec.getSnippetCountPerProperty()) 439 .setMaxWindowUtf32Length(mSearchSpec.getMaxSnippetSize()) 440 .setGetEmbeddingMatchInfo( 441 mSearchSpec.shouldRetrieveEmbeddingMatchInfos())) 442 .setNumTotalBytesPerPageThreshold(mIcingOptionsConfig.getMaxPageBytesLimit()); 443 JoinSpec joinSpec = mSearchSpec.getJoinSpec(); 444 if (joinSpec != null) { 445 resultSpecBuilder.setMaxJoinedChildrenPerParentToReturn( 446 joinSpec.getMaxJoinedResultCount()); 447 } 448 449 // Add result groupings for the available prefixes 450 int groupingType = mSearchSpec.getResultGroupingTypeFlags(); 451 ResultSpecProto.ResultGroupingType resultGroupingType = 452 ResultSpecProto.ResultGroupingType.NONE; 453 switch (groupingType) { 454 case SearchSpec.GROUPING_TYPE_PER_PACKAGE : 455 addPerPackageResultGroupings(mCurrentSearchSpecPrefixFilters, 456 mSearchSpec.getResultGroupingLimit(), namespaceCache, resultSpecBuilder); 457 resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE; 458 break; 459 case SearchSpec.GROUPING_TYPE_PER_NAMESPACE: 460 addPerNamespaceResultGroupings(mCurrentSearchSpecPrefixFilters, 461 mSearchSpec.getResultGroupingLimit(), namespaceCache, resultSpecBuilder); 462 resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE; 463 break; 464 case SearchSpec.GROUPING_TYPE_PER_SCHEMA: 465 addPerSchemaResultGrouping(mCurrentSearchSpecPrefixFilters, 466 mSearchSpec.getResultGroupingLimit(), schemaCache, resultSpecBuilder); 467 resultGroupingType = ResultSpecProto.ResultGroupingType.SCHEMA_TYPE; 468 break; 469 case SearchSpec.GROUPING_TYPE_PER_PACKAGE | SearchSpec.GROUPING_TYPE_PER_NAMESPACE: 470 addPerPackagePerNamespaceResultGroupings(mCurrentSearchSpecPrefixFilters, 471 mSearchSpec.getResultGroupingLimit(), 472 namespaceCache, resultSpecBuilder); 473 resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE; 474 break; 475 case SearchSpec.GROUPING_TYPE_PER_PACKAGE | SearchSpec.GROUPING_TYPE_PER_SCHEMA: 476 addPerPackagePerSchemaResultGroupings(mCurrentSearchSpecPrefixFilters, 477 mSearchSpec.getResultGroupingLimit(), 478 schemaCache, resultSpecBuilder); 479 resultGroupingType = ResultSpecProto.ResultGroupingType.SCHEMA_TYPE; 480 break; 481 case SearchSpec.GROUPING_TYPE_PER_NAMESPACE | SearchSpec.GROUPING_TYPE_PER_SCHEMA: 482 addPerNamespaceAndSchemaResultGrouping(mCurrentSearchSpecPrefixFilters, 483 mSearchSpec.getResultGroupingLimit(), 484 namespaceCache, schemaCache, resultSpecBuilder); 485 resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE_AND_SCHEMA_TYPE; 486 break; 487 case SearchSpec.GROUPING_TYPE_PER_PACKAGE | SearchSpec.GROUPING_TYPE_PER_NAMESPACE 488 | SearchSpec.GROUPING_TYPE_PER_SCHEMA: 489 addPerPackagePerNamespacePerSchemaResultGrouping(mCurrentSearchSpecPrefixFilters, 490 mSearchSpec.getResultGroupingLimit(), 491 namespaceCache, schemaCache, resultSpecBuilder); 492 resultGroupingType = ResultSpecProto.ResultGroupingType.NAMESPACE_AND_SCHEMA_TYPE; 493 break; 494 default: 495 break; 496 } 497 resultSpecBuilder.setResultGroupType(resultGroupingType); 498 499 List<TypePropertyMask.Builder> typePropertyMaskBuilders = 500 TypePropertyPathToProtoConverter 501 .toTypePropertyMaskBuilderList(mSearchSpec.getProjections()); 502 // Rewrite filters to include a database prefix. 503 for (int i = 0; i < typePropertyMaskBuilders.size(); i++) { 504 String unprefixedType = typePropertyMaskBuilders.get(i).getSchemaType(); 505 if (unprefixedType.equals(SearchSpec.SCHEMA_TYPE_WILDCARD)) { 506 resultSpecBuilder.addTypePropertyMasks(typePropertyMaskBuilders.get(i).build()); 507 } else { 508 // Qualify the given schema types 509 for (String prefix : mCurrentSearchSpecPrefixFilters) { 510 String prefixedType = prefix + unprefixedType; 511 if (mTargetPrefixedSchemaFilters.contains(prefixedType)) { 512 resultSpecBuilder.addTypePropertyMasks(typePropertyMaskBuilders.get(i) 513 .setSchemaType(prefixedType).build()); 514 } 515 } 516 } 517 } 518 519 return resultSpecBuilder.build(); 520 } 521 522 /** Extracts {@link ScoringSpecProto} information from a {@link SearchSpec}. */ 523 @OptIn(markerClass = ExperimentalAppSearchApi.class) toScoringSpecProto()524 public @NonNull ScoringSpecProto toScoringSpecProto() { 525 ScoringSpecProto.Builder protoBuilder = ScoringSpecProto.newBuilder(); 526 527 @SearchSpec.Order int orderCode = mSearchSpec.getOrder(); 528 ScoringSpecProto.Order.Code orderCodeProto = 529 ScoringSpecProto.Order.Code.forNumber(orderCode); 530 if (orderCodeProto == null) { 531 throw new IllegalArgumentException("Invalid result ranking order: " + orderCode); 532 } 533 protoBuilder.setOrderBy(orderCodeProto).setRankBy( 534 toProtoRankingStrategy(mSearchSpec.getRankingStrategy())); 535 536 addTypePropertyWeights(mSearchSpec.getPropertyWeights(), protoBuilder); 537 538 protoBuilder.setAdvancedScoringExpression(mSearchSpec.getAdvancedRankingExpression()); 539 protoBuilder.addAllAdditionalAdvancedScoringExpressions( 540 mSearchSpec.getInformationalRankingExpressions()); 541 542 // TODO(b/380924970): create extractEnabledScoringFeatures() for populating scorable 543 // features 544 if (mSearchSpec.isScorablePropertyRankingEnabled()) { 545 protoBuilder.addScoringFeatureTypesEnabled( 546 ScoringFeatureType.SCORABLE_PROPERTY_RANKING); 547 Map<String, Set<String>> schemaToPrefixedSchemasMap = createSchemaToPrefixedSchemasMap( 548 mTargetPrefixedSchemaFilters); 549 for (Map.Entry<String, Set<String>> entry : schemaToPrefixedSchemasMap.entrySet()) { 550 SchemaTypeAliasMapProto.Builder schemaTypeAliasMapProto = 551 SchemaTypeAliasMapProto.newBuilder(); 552 schemaTypeAliasMapProto.setAliasSchemaType(entry.getKey()); 553 schemaTypeAliasMapProto.addAllSchemaTypes(entry.getValue()); 554 protoBuilder.addSchemaTypeAliasMapProtos(schemaTypeAliasMapProto); 555 } 556 } 557 558 return protoBuilder.build(); 559 } 560 toProtoRankingStrategy( @earchSpec.RankingStrategy int rankingStrategyCode)561 private static ScoringSpecProto.RankingStrategy.Code toProtoRankingStrategy( 562 @SearchSpec.RankingStrategy int rankingStrategyCode) { 563 switch (rankingStrategyCode) { 564 case SearchSpec.RANKING_STRATEGY_NONE: 565 return ScoringSpecProto.RankingStrategy.Code.NONE; 566 case SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE: 567 return ScoringSpecProto.RankingStrategy.Code.DOCUMENT_SCORE; 568 case SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP: 569 return ScoringSpecProto.RankingStrategy.Code.CREATION_TIMESTAMP; 570 case SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE: 571 return ScoringSpecProto.RankingStrategy.Code.RELEVANCE_SCORE; 572 case SearchSpec.RANKING_STRATEGY_USAGE_COUNT: 573 return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE1_COUNT; 574 case SearchSpec.RANKING_STRATEGY_USAGE_LAST_USED_TIMESTAMP: 575 return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE1_LAST_USED_TIMESTAMP; 576 case SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_COUNT: 577 return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE2_COUNT; 578 case SearchSpec.RANKING_STRATEGY_SYSTEM_USAGE_LAST_USED_TIMESTAMP: 579 return ScoringSpecProto.RankingStrategy.Code.USAGE_TYPE2_LAST_USED_TIMESTAMP; 580 case SearchSpec.RANKING_STRATEGY_ADVANCED_RANKING_EXPRESSION: 581 return ScoringSpecProto.RankingStrategy.Code.ADVANCED_SCORING_EXPRESSION; 582 case SearchSpec.RANKING_STRATEGY_JOIN_AGGREGATE_SCORE: 583 return ScoringSpecProto.RankingStrategy.Code.JOIN_AGGREGATE_SCORE; 584 default: 585 throw new IllegalArgumentException("Invalid result ranking strategy: " 586 + rankingStrategyCode); 587 } 588 } 589 590 /** 591 * Maps a list of AppSearch search feature strings to the list of the corresponding Icing 592 * feature strings. 593 * 594 * @param appSearchFeatures The list of AppSearch search feature strings. 595 */ toIcingSearchFeatures( @onNull List<String> appSearchFeatures)596 private static @NonNull List<String> toIcingSearchFeatures( 597 @NonNull List<String> appSearchFeatures) { 598 List<String> result = new ArrayList<>(); 599 for (int i = 0; i < appSearchFeatures.size(); i++) { 600 String appSearchFeature = appSearchFeatures.get(i); 601 if (appSearchFeature.equals(FeatureConstants.LIST_FILTER_HAS_PROPERTY_FUNCTION)) { 602 result.add("HAS_PROPERTY_FUNCTION"); 603 } else if (appSearchFeature.equals( 604 FeatureConstants.LIST_FILTER_MATCH_SCORE_EXPRESSION_FUNCTION)) { 605 result.add("MATCH_SCORE_EXPRESSION_FUNCTION"); 606 } else { 607 result.add(appSearchFeature); 608 } 609 } 610 return result; 611 } 612 613 /** 614 * Returns a Map of namespace to prefixedNamespaces. This is NOT necessarily the 615 * same as the list of namespaces. If a namespace exists under different packages and/or 616 * different databases, they should still be grouped together. 617 * 618 * @param prefixes Prefixes that we should prepend to all our filters. 619 * @param namespaceCache The NamespaceCache instance held in AppSearch. 620 */ getNamespaceToPrefixedNamespaces( @onNull Set<String> prefixes, @NonNull NamespaceCache namespaceCache)621 private static Map<String, List<String>> getNamespaceToPrefixedNamespaces( 622 @NonNull Set<String> prefixes, 623 @NonNull NamespaceCache namespaceCache) { 624 Map<String, List<String>> namespaceToPrefixedNamespaces = new ArrayMap<>(); 625 for (String prefix : prefixes) { 626 Set<String> prefixedNamespaces = namespaceCache.getPrefixedDocumentNamespaces(prefix); 627 if (prefixedNamespaces == null) { 628 continue; 629 } 630 for (String prefixedNamespace : prefixedNamespaces) { 631 String namespace; 632 try { 633 namespace = removePrefix(prefixedNamespace); 634 } catch (AppSearchException e) { 635 // This should never happen. Skip this namespace if it does. 636 Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed."); 637 continue; 638 } 639 List<String> groupedPrefixedNamespaces = 640 namespaceToPrefixedNamespaces.get(namespace); 641 if (groupedPrefixedNamespaces == null) { 642 groupedPrefixedNamespaces = new ArrayList<>(); 643 namespaceToPrefixedNamespaces.put(namespace, groupedPrefixedNamespaces); 644 } 645 groupedPrefixedNamespaces.add(prefixedNamespace); 646 } 647 } 648 return namespaceToPrefixedNamespaces; 649 } 650 651 /** 652 * Returns a map for package+namespace to prefixedNamespaces. This is NOT necessarily the 653 * same as the list of namespaces. If one package has multiple databases, each with the same 654 * namespace, then those should be grouped together. 655 * 656 * @param prefixes Prefixes that we should prepend to all our filters. 657 * @param namespaceCache The NamespaceCache instance held in AppSearch. 658 */ getPackageAndNamespaceToPrefixedNamespaces( @onNull Set<String> prefixes, @NonNull NamespaceCache namespaceCache)659 private static Map<String, List<String>> getPackageAndNamespaceToPrefixedNamespaces( 660 @NonNull Set<String> prefixes, 661 @NonNull NamespaceCache namespaceCache) { 662 Map<String, List<String>> packageAndNamespaceToNamespaces = new ArrayMap<>(); 663 for (String prefix : prefixes) { 664 Set<String> prefixedNamespaces = namespaceCache.getPrefixedDocumentNamespaces(prefix); 665 if (prefixedNamespaces == null) { 666 continue; 667 } 668 String packageName = getPackageName(prefix); 669 // Create a new prefix without the database name. This will allow us to group namespaces 670 // that have the same name and package but a different database name together. 671 String emptyDatabasePrefix = createPrefix(packageName, /* databaseName= */""); 672 for (String prefixedNamespace : prefixedNamespaces) { 673 String namespace; 674 try { 675 namespace = removePrefix(prefixedNamespace); 676 } catch (AppSearchException e) { 677 // This should never happen. Skip this namespace if it does. 678 Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed."); 679 continue; 680 } 681 String emptyDatabasePrefixedNamespace = emptyDatabasePrefix + namespace; 682 List<String> namespaceList = 683 packageAndNamespaceToNamespaces.get(emptyDatabasePrefixedNamespace); 684 if (namespaceList == null) { 685 namespaceList = new ArrayList<>(); 686 packageAndNamespaceToNamespaces.put(emptyDatabasePrefixedNamespace, 687 namespaceList); 688 } 689 namespaceList.add(prefixedNamespace); 690 } 691 } 692 return packageAndNamespaceToNamespaces; 693 } 694 695 /** 696 * Returns a map of schema to prefixedSchemas. This is NOT necessarily the 697 * same as the list of schemas. If a schema exists under different packages and/or 698 * different databases, they should still be grouped together. 699 * 700 * @param prefixes Prefixes that we should prepend to all our filters. 701 * @param schemaCache The SchemaCache instance held in AppSearch. 702 */ getSchemaToPrefixedSchemas( @onNull Set<String> prefixes, @NonNull SchemaCache schemaCache)703 private static Map<String, List<String>> getSchemaToPrefixedSchemas( 704 @NonNull Set<String> prefixes, 705 @NonNull SchemaCache schemaCache) { 706 Map<String, List<String>> schemaToPrefixedSchemas = new ArrayMap<>(); 707 for (String prefix : prefixes) { 708 Map<String, SchemaTypeConfigProto> prefixedSchemas = 709 schemaCache.getSchemaMapForPrefix(prefix); 710 for (String prefixedSchema : prefixedSchemas.keySet()) { 711 String schema; 712 try { 713 schema = removePrefix(prefixedSchema); 714 } catch (AppSearchException e) { 715 // This should never happen. Skip this schema if it does. 716 Log.e(TAG, "Prefixed schema " + prefixedSchema + " is malformed."); 717 continue; 718 } 719 List<String> groupedPrefixedSchemas = 720 schemaToPrefixedSchemas.get(schema); 721 if (groupedPrefixedSchemas == null) { 722 groupedPrefixedSchemas = new ArrayList<>(); 723 schemaToPrefixedSchemas.put(schema, groupedPrefixedSchemas); 724 } 725 groupedPrefixedSchemas.add(prefixedSchema); 726 } 727 } 728 return schemaToPrefixedSchemas; 729 } 730 731 /** 732 * Returns a map of schema to a set of prefixedSchemas, grouped by ending schema string. 733 * 734 * For example, an input of 735 * { 736 * "package1$database1/gmail", "package1$database2/gmail", 737 * "package1$database1/person", "package1$database2/person"} 738 * will return an output of: 739 * { 740 * "gmail": {"package1$database1/gmail", "package1$database2/gmail"}, 741 * "person": {"package1$database1/person", "package1$database2/person"}, 742 * } 743 */ createSchemaToPrefixedSchemasMap( Set<String> prefixedSchemas)744 private Map<String, Set<String>> createSchemaToPrefixedSchemasMap( 745 Set<String> prefixedSchemas) { 746 Map<String, Set<String>> schemasToPrefixedSchemas = new ArrayMap<>(); 747 for (String prefixedSchema : prefixedSchemas) { 748 String schema; 749 try { 750 schema = removePrefix(prefixedSchema); 751 } catch (AppSearchException e) { 752 // This should never happen. Skip this schema if it does. 753 Log.e(TAG, "Prefixed schema " + prefixedSchema + " is malformed."); 754 continue; 755 } 756 Set<String> prefixedSchemaSet = 757 schemasToPrefixedSchemas.get(schema); 758 if (prefixedSchemaSet == null) { 759 prefixedSchemaSet = new ArraySet<>(); 760 schemasToPrefixedSchemas.put(schema, prefixedSchemaSet); 761 } 762 prefixedSchemaSet.add(prefixedSchema); 763 } 764 return schemasToPrefixedSchemas; 765 } 766 767 /** 768 * Returns a map for package+schema to prefixedSchemas. This is NOT necessarily the 769 * same as the list of schemas. If one package has multiple databases, each with the same 770 * schema, then those should be grouped together. 771 * 772 * @param prefixes Prefixes that we should prepend to all our filters. 773 * @param schemaCache The SchemaCache instance held in AppSearch. 774 */ getPackageAndSchemaToPrefixedSchemas( @onNull Set<String> prefixes, @NonNull SchemaCache schemaCache)775 private static Map<String, List<String>> getPackageAndSchemaToPrefixedSchemas( 776 @NonNull Set<String> prefixes, 777 @NonNull SchemaCache schemaCache) { 778 Map<String, List<String>> packageAndSchemaToSchemas = new ArrayMap<>(); 779 for (String prefix : prefixes) { 780 Map<String, SchemaTypeConfigProto> prefixedSchemas = 781 schemaCache.getSchemaMapForPrefix(prefix); 782 String packageName = getPackageName(prefix); 783 // Create a new prefix without the database name. This will allow us to group schemas 784 // that have the same name and package but a different database name together. 785 String emptyDatabasePrefix = createPrefix(packageName, /*database*/""); 786 for (String prefixedSchema : prefixedSchemas.keySet()) { 787 String schema; 788 try { 789 schema = removePrefix(prefixedSchema); 790 } catch (AppSearchException e) { 791 // This should never happen. Skip this schema if it does. 792 Log.e(TAG, "Prefixed schema " + prefixedSchema + " is malformed."); 793 continue; 794 } 795 String emptyDatabasePrefixedSchema = emptyDatabasePrefix + schema; 796 List<String> schemaList = 797 packageAndSchemaToSchemas.get(emptyDatabasePrefixedSchema); 798 if (schemaList == null) { 799 schemaList = new ArrayList<>(); 800 packageAndSchemaToSchemas.put(emptyDatabasePrefixedSchema, schemaList); 801 } 802 schemaList.add(prefixedSchema); 803 } 804 } 805 return packageAndSchemaToSchemas; 806 } 807 808 /** 809 * Adds result groupings for each namespace in each package being queried for. 810 * 811 * @param prefixes Prefixes that we should prepend to all our filters 812 * @param maxNumResults The maximum number of results for each grouping to support. 813 * @param namespaceCache The NamespaceCache instance held in AppSearch. 814 * @param resultSpecBuilder ResultSpecs as specified by client 815 */ addPerPackagePerNamespaceResultGroupings( @onNull Set<String> prefixes, int maxNumResults, @NonNull NamespaceCache namespaceCache, ResultSpecProto.@NonNull Builder resultSpecBuilder)816 private static void addPerPackagePerNamespaceResultGroupings( 817 @NonNull Set<String> prefixes, 818 int maxNumResults, 819 @NonNull NamespaceCache namespaceCache, 820 ResultSpecProto.@NonNull Builder resultSpecBuilder) { 821 Map<String, List<String>> packageAndNamespaceToNamespaces = 822 getPackageAndNamespaceToPrefixedNamespaces(prefixes, namespaceCache); 823 824 for (List<String> prefixedNamespaces : packageAndNamespaceToNamespaces.values()) { 825 List<ResultSpecProto.ResultGrouping.Entry> entries = 826 new ArrayList<>(prefixedNamespaces.size()); 827 for (int i = 0; i < prefixedNamespaces.size(); i++) { 828 entries.add( 829 ResultSpecProto.ResultGrouping.Entry.newBuilder() 830 .setNamespace(prefixedNamespaces.get(i)).build()); 831 } 832 resultSpecBuilder.addResultGroupings( 833 ResultSpecProto.ResultGrouping.newBuilder() 834 .addAllEntryGroupings(entries).setMaxResults(maxNumResults)); 835 } 836 } 837 838 /** 839 * Adds result groupings for each schema type in each package being queried for. 840 * 841 * @param prefixes Prefixes that we should prepend to all our filters. 842 * @param maxNumResults The maximum number of results for each grouping to support. 843 * @param schemaCache The SchemaCache instance held in AppSearch. 844 * @param resultSpecBuilder ResultSpecs as a specified by client. 845 */ addPerPackagePerSchemaResultGroupings( @onNull Set<String> prefixes, int maxNumResults, @NonNull SchemaCache schemaCache, ResultSpecProto.@NonNull Builder resultSpecBuilder)846 private static void addPerPackagePerSchemaResultGroupings( 847 @NonNull Set<String> prefixes, 848 int maxNumResults, 849 @NonNull SchemaCache schemaCache, 850 ResultSpecProto.@NonNull Builder resultSpecBuilder) { 851 Map<String, List<String>> packageAndSchemaToSchemas = 852 getPackageAndSchemaToPrefixedSchemas(prefixes, schemaCache); 853 854 for (List<String> prefixedSchemas : packageAndSchemaToSchemas.values()) { 855 List<ResultSpecProto.ResultGrouping.Entry> entries = 856 new ArrayList<>(prefixedSchemas.size()); 857 for (int i = 0; i < prefixedSchemas.size(); i++) { 858 entries.add( 859 ResultSpecProto.ResultGrouping.Entry.newBuilder() 860 .setSchema(prefixedSchemas.get(i)).build()); 861 } 862 resultSpecBuilder.addResultGroupings( 863 ResultSpecProto.ResultGrouping.newBuilder() 864 .addAllEntryGroupings(entries).setMaxResults(maxNumResults)); 865 } 866 } 867 868 /** 869 * Adds result groupings for each namespace and schema type being queried for. 870 * 871 * @param prefixes Prefixes that we should prepend to all our filters. 872 * @param maxNumResults The maximum number of results for each grouping to support. 873 * @param namespaceCache The NamespaceCache instance held in AppSearch. 874 * @param schemaCache The SchemaCache instance held in AppSearch. 875 * @param resultSpecBuilder ResultSpec as specified by client. 876 */ addPerPackagePerNamespacePerSchemaResultGrouping( @onNull Set<String> prefixes, int maxNumResults, @NonNull NamespaceCache namespaceCache, @NonNull SchemaCache schemaCache, ResultSpecProto.@NonNull Builder resultSpecBuilder)877 private static void addPerPackagePerNamespacePerSchemaResultGrouping( 878 @NonNull Set<String> prefixes, 879 int maxNumResults, 880 @NonNull NamespaceCache namespaceCache, 881 @NonNull SchemaCache schemaCache, 882 ResultSpecProto.@NonNull Builder resultSpecBuilder) { 883 Map<String, List<String>> packageAndNamespaceToNamespaces = 884 getPackageAndNamespaceToPrefixedNamespaces(prefixes, namespaceCache); 885 Map<String, List<String>> packageAndSchemaToSchemas = 886 getPackageAndSchemaToPrefixedSchemas(prefixes, schemaCache); 887 888 for (List<String> prefixedNamespaces : packageAndNamespaceToNamespaces.values()) { 889 for (List<String> prefixedSchemas : packageAndSchemaToSchemas.values()) { 890 List<ResultSpecProto.ResultGrouping.Entry> entries = 891 new ArrayList<>(prefixedNamespaces.size() * prefixedSchemas.size()); 892 // Iterate through all namespaces. 893 for (int i = 0; i < prefixedNamespaces.size(); i++) { 894 String namespacePackage = getPackageName(prefixedNamespaces.get(i)); 895 // Iterate through all schemas. 896 for (int j = 0; j < prefixedSchemas.size(); j++) { 897 String schemaPackage = getPackageName(prefixedSchemas.get(j)); 898 if (namespacePackage.equals(schemaPackage)) { 899 entries.add( 900 ResultSpecProto.ResultGrouping.Entry.newBuilder() 901 .setNamespace(prefixedNamespaces.get(i)) 902 .setSchema(prefixedSchemas.get(j)) 903 .build()); 904 } 905 } 906 } 907 if (entries.size() > 0) { 908 resultSpecBuilder.addResultGroupings( 909 ResultSpecProto.ResultGrouping.newBuilder() 910 .addAllEntryGroupings(entries).setMaxResults(maxNumResults)); 911 } 912 } 913 } 914 } 915 916 /** 917 * Adds result groupings for each package being queried for. 918 * 919 * @param prefixes Prefixes that we should prepend to all our filters 920 * @param maxNumResults The maximum number of results for each grouping to support. 921 * @param namespaceCache The NamespaceCache instance held in AppSearch. 922 * @param resultSpecBuilder ResultSpecs as specified by client 923 */ addPerPackageResultGroupings( @onNull Set<String> prefixes, int maxNumResults, @NonNull NamespaceCache namespaceCache, ResultSpecProto.@NonNull Builder resultSpecBuilder)924 private static void addPerPackageResultGroupings( 925 @NonNull Set<String> prefixes, 926 int maxNumResults, 927 @NonNull NamespaceCache namespaceCache, 928 ResultSpecProto.@NonNull Builder resultSpecBuilder) { 929 // Build up a map of package to namespaces. 930 Map<String, List<String>> packageToNamespacesMap = new ArrayMap<>(); 931 for (String prefix : prefixes) { 932 Set<String> prefixedNamespaces = namespaceCache.getPrefixedDocumentNamespaces(prefix); 933 if (prefixedNamespaces == null) { 934 continue; 935 } 936 String packageName = getPackageName(prefix); 937 List<String> packageNamespaceList = packageToNamespacesMap.get(packageName); 938 if (packageNamespaceList == null) { 939 packageNamespaceList = new ArrayList<>(); 940 packageToNamespacesMap.put(packageName, packageNamespaceList); 941 } 942 packageNamespaceList.addAll(prefixedNamespaces); 943 } 944 945 for (List<String> prefixedNamespaces : packageToNamespacesMap.values()) { 946 List<ResultSpecProto.ResultGrouping.Entry> entries = 947 new ArrayList<>(prefixedNamespaces.size()); 948 for (String namespace : prefixedNamespaces) { 949 entries.add( 950 ResultSpecProto.ResultGrouping.Entry.newBuilder() 951 .setNamespace(namespace).build()); 952 } 953 resultSpecBuilder.addResultGroupings( 954 ResultSpecProto.ResultGrouping.newBuilder() 955 .addAllEntryGroupings(entries).setMaxResults(maxNumResults)); 956 } 957 } 958 959 /** 960 * Adds result groupings for each namespace being queried for. 961 * 962 * @param prefixes Prefixes that we should prepend to all our filters 963 * @param maxNumResults The maximum number of results for each grouping to support. 964 * @param namespaceCache The NamespaceCache instance held in AppSearch. 965 * @param resultSpecBuilder ResultSpecs as specified by client 966 */ addPerNamespaceResultGroupings( @onNull Set<String> prefixes, int maxNumResults, @NonNull NamespaceCache namespaceCache, ResultSpecProto.@NonNull Builder resultSpecBuilder)967 private static void addPerNamespaceResultGroupings( 968 @NonNull Set<String> prefixes, 969 int maxNumResults, 970 @NonNull NamespaceCache namespaceCache, 971 ResultSpecProto.@NonNull Builder resultSpecBuilder) { 972 Map<String, List<String>> namespaceToPrefixedNamespaces = 973 getNamespaceToPrefixedNamespaces(prefixes, namespaceCache); 974 975 for (List<String> prefixedNamespaces : namespaceToPrefixedNamespaces.values()) { 976 List<ResultSpecProto.ResultGrouping.Entry> entries = 977 new ArrayList<>(prefixedNamespaces.size()); 978 for (int i = 0; i < prefixedNamespaces.size(); i++) { 979 entries.add( 980 ResultSpecProto.ResultGrouping.Entry.newBuilder() 981 .setNamespace(prefixedNamespaces.get(i)).build()); 982 } 983 resultSpecBuilder.addResultGroupings( 984 ResultSpecProto.ResultGrouping.newBuilder() 985 .addAllEntryGroupings(entries).setMaxResults(maxNumResults)); 986 } 987 } 988 989 /** 990 * Adds result groupings for each schema type being queried for. 991 * 992 * @param prefixes Prefixes that we should prepend to all our filters. 993 * @param maxNumResults The maximum number of results for each grouping to support. 994 * @param schemaCache The SchemaCache instance held in AppSearch. 995 * @param resultSpecBuilder ResultSpec as specified by client. 996 */ addPerSchemaResultGrouping( @onNull Set<String> prefixes, int maxNumResults, @NonNull SchemaCache schemaCache, ResultSpecProto.@NonNull Builder resultSpecBuilder)997 private static void addPerSchemaResultGrouping( 998 @NonNull Set<String> prefixes, 999 int maxNumResults, 1000 @NonNull SchemaCache schemaCache, 1001 ResultSpecProto.@NonNull Builder resultSpecBuilder) { 1002 Map<String, List<String>> schemaToPrefixedSchemas = 1003 getSchemaToPrefixedSchemas(prefixes, schemaCache); 1004 1005 for (List<String> prefixedSchemas : schemaToPrefixedSchemas.values()) { 1006 List<ResultSpecProto.ResultGrouping.Entry> entries = 1007 new ArrayList<>(prefixedSchemas.size()); 1008 for (int i = 0; i < prefixedSchemas.size(); i++) { 1009 entries.add( 1010 ResultSpecProto.ResultGrouping.Entry.newBuilder() 1011 .setSchema(prefixedSchemas.get(i)).build()); 1012 } 1013 resultSpecBuilder.addResultGroupings( 1014 ResultSpecProto.ResultGrouping.newBuilder() 1015 .addAllEntryGroupings(entries).setMaxResults(maxNumResults)); 1016 } 1017 } 1018 1019 /** 1020 * Adds result groupings for each namespace and schema type being queried for. 1021 * 1022 * @param prefixes Prefixes that we should prepend to all our filters. 1023 * @param maxNumResults The maximum number of results for each grouping to support. 1024 * @param namespaceCache The NamespaceCache instance held in AppSearch. 1025 * @param schemaCache The SchemaCache instance held in AppSearch. 1026 * @param resultSpecBuilder ResultSpec as specified by client. 1027 */ addPerNamespaceAndSchemaResultGrouping( @onNull Set<String> prefixes, int maxNumResults, @NonNull NamespaceCache namespaceCache, @NonNull SchemaCache schemaCache, ResultSpecProto.@NonNull Builder resultSpecBuilder)1028 private static void addPerNamespaceAndSchemaResultGrouping( 1029 @NonNull Set<String> prefixes, 1030 int maxNumResults, 1031 @NonNull NamespaceCache namespaceCache, 1032 @NonNull SchemaCache schemaCache, 1033 ResultSpecProto.@NonNull Builder resultSpecBuilder) { 1034 Map<String, List<String>> namespaceToPrefixedNamespaces = 1035 getNamespaceToPrefixedNamespaces(prefixes, namespaceCache); 1036 Map<String, List<String>> schemaToPrefixedSchemas = 1037 getSchemaToPrefixedSchemas(prefixes, schemaCache); 1038 1039 for (List<String> prefixedNamespaces : namespaceToPrefixedNamespaces.values()) { 1040 for (List<String> prefixedSchemas : schemaToPrefixedSchemas.values()) { 1041 List<ResultSpecProto.ResultGrouping.Entry> entries = 1042 new ArrayList<>(prefixedNamespaces.size() * prefixedSchemas.size()); 1043 // Iterate through all namespaces. 1044 for (int i = 0; i < prefixedNamespaces.size(); i++) { 1045 // Iterate through all schemas. 1046 for (int j = 0; j < prefixedSchemas.size(); j++) { 1047 try { 1048 if (getPrefix(prefixedNamespaces.get(i)) 1049 .equals(getPrefix(prefixedSchemas.get(j)))) { 1050 entries.add( 1051 ResultSpecProto.ResultGrouping.Entry.newBuilder() 1052 .setNamespace(prefixedNamespaces.get(i)) 1053 .setSchema(prefixedSchemas.get(j)) 1054 .build()); 1055 } 1056 } catch (AppSearchException e) { 1057 // This should never happen. Skip this schema if it does. 1058 Log.e(TAG, "Prefixed string " + prefixedNamespaces.get(i) + " or " 1059 + prefixedSchemas.get(j) + " is malformed."); 1060 continue; 1061 } 1062 } 1063 } 1064 if (entries.size() > 0) { 1065 resultSpecBuilder.addResultGroupings( 1066 ResultSpecProto.ResultGrouping.newBuilder() 1067 .addAllEntryGroupings(entries).setMaxResults(maxNumResults)); 1068 } 1069 } 1070 } 1071 } 1072 1073 /** 1074 * Adds {@link TypePropertyWeights} to {@link ScoringSpecProto}. 1075 * 1076 * <p>{@link TypePropertyWeights} are added to the {@link ScoringSpecProto} with database and 1077 * package prefixing added to the schema type. 1078 * 1079 * @param typePropertyWeightsMap a map from unprefixed schema type to an inner-map of property 1080 * paths to weight. 1081 * @param scoringSpecBuilder scoring spec to add weights to. 1082 */ addTypePropertyWeights( @onNull Map<String, Map<String, Double>> typePropertyWeightsMap, ScoringSpecProto.@NonNull Builder scoringSpecBuilder)1083 private void addTypePropertyWeights( 1084 @NonNull Map<String, Map<String, Double>> typePropertyWeightsMap, 1085 ScoringSpecProto.@NonNull Builder scoringSpecBuilder) { 1086 Preconditions.checkNotNull(scoringSpecBuilder); 1087 Preconditions.checkNotNull(typePropertyWeightsMap); 1088 1089 for (Map.Entry<String, Map<String, Double>> typePropertyWeight : 1090 typePropertyWeightsMap.entrySet()) { 1091 for (String prefix : mCurrentSearchSpecPrefixFilters) { 1092 String prefixedSchemaType = prefix + typePropertyWeight.getKey(); 1093 if (mTargetPrefixedSchemaFilters.contains(prefixedSchemaType)) { 1094 TypePropertyWeights.Builder typePropertyWeightsBuilder = 1095 TypePropertyWeights.newBuilder().setSchemaType(prefixedSchemaType); 1096 1097 for (Map.Entry<String, Double> propertyWeight : 1098 typePropertyWeight.getValue().entrySet()) { 1099 typePropertyWeightsBuilder.addPropertyWeights( 1100 PropertyWeight.newBuilder().setPath( 1101 propertyWeight.getKey()).setWeight( 1102 propertyWeight.getValue())); 1103 } 1104 1105 scoringSpecBuilder.addTypePropertyWeights(typePropertyWeightsBuilder); 1106 } 1107 } 1108 } 1109 } 1110 extractEnabledSearchFeatures(List<String> allEnabledFeatures)1111 List<String> extractEnabledSearchFeatures(List<String> allEnabledFeatures) { 1112 List<String> searchFeatures = new ArrayList<>(); 1113 for (int i = 0; i < allEnabledFeatures.size(); ++i) { 1114 String feature = allEnabledFeatures.get(i); 1115 if (FeatureConstants.SCORABLE_FEATURE_SET.contains(feature)) { 1116 // The `allEnabledFeatures` set contains both scorable features and search features. 1117 // Here, we extract the search related features and populate them to 1118 // `SearchSpecProto`. The scoring related features are later populated to the 1119 // `ScoringSpecProto` individually in `toScoringSpecProto()`. 1120 // - This is because in Icing, the search expression and scoring expression are 1121 // parsed separately, and the enforcement of these enabled features are separate. 1122 // Icing needs the two different proto messages to distinguish between 1123 // features in the search expression and features in the scoring expression. 1124 continue; 1125 } 1126 searchFeatures.add(feature); 1127 } 1128 return searchFeatures; 1129 } 1130 } 1131