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