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