1 /* 2 * Copyright 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.app.appsearch; 18 19 import android.annotation.FlaggedApi; 20 import android.annotation.IntDef; 21 import android.annotation.NonNull; 22 import android.app.appsearch.annotation.CanIgnoreReturnValue; 23 import android.app.appsearch.safeparcel.AbstractSafeParcelable; 24 import android.app.appsearch.safeparcel.SafeParcelable; 25 import android.os.Parcel; 26 import android.os.Parcelable; 27 28 import com.android.appsearch.flags.Flags; 29 import com.android.internal.util.Preconditions; 30 31 import java.lang.annotation.Retention; 32 import java.lang.annotation.RetentionPolicy; 33 import java.util.Objects; 34 35 /** 36 * This class represents the specifications for the joining operation in search. 37 * 38 * <p>Joins are only possible for matching on the qualified id of an outer document and a property 39 * value within a subquery document. In the subquery documents, these values may be referred to with 40 * a property path such as "email.recipient.id" or "entityId" or a property expression. One such 41 * property expression is "this.qualifiedId()", which refers to the document's combined package, 42 * database, namespace, and id. 43 * 44 * <p>Note that in order for perform the join, the property referred to by {@link 45 * #getChildPropertyExpression} has to be a property with {@link 46 * AppSearchSchema.StringPropertyConfig#getJoinableValueType} set to {@link 47 * AppSearchSchema.StringPropertyConfig#JOINABLE_VALUE_TYPE_QUALIFIED_ID}. Otherwise no documents 48 * will be joined to any {@link SearchResult}. 49 * 50 * <p>Take these outer query and subquery results for example: 51 * 52 * <pre>{@code 53 * Outer result { 54 * id: id1 55 * score: 5 56 * } 57 * Subquery result 1 { 58 * id: id2 59 * score: 2 60 * entityId: pkg$db/ns#id1 61 * notes: This is some doc 62 * } 63 * Subquery result 2 { 64 * id: id3 65 * score: 3 66 * entityId: pkg$db/ns#id2 67 * notes: This is another doc 68 * } 69 * }</pre> 70 * 71 * <p>In this example, subquery result 1 contains a property "entityId" whose value is 72 * "pkg$db/ns#id1", referring to the outer result. If you call {@link Builder} with "entityId", we 73 * will retrieve the value of the property "entityId" from the child document, which is 74 * "pkg$db#ns/id1". Let's say the qualified id of the outer result is "pkg$db#ns/id1". This would 75 * mean the subquery result 1 document will be matched to that parent document. This is done by 76 * adding a {@link SearchResult} containing the child document to the top-level parent {@link 77 * SearchResult#getJoinedResults}. 78 * 79 * <p>If {@link #getChildPropertyExpression} is "notes", we will check the values of the notes 80 * property in the subquery results. In subquery result 1, this values is "This is some doc", which 81 * does not equal the qualified id of the outer query result. As such, subquery result 1 will not be 82 * joined to the outer query result. 83 * 84 * <p>It's possible to define an advanced ranking strategy in the nested {@link SearchSpec} and also 85 * use {@link SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE} in the outer {@link SearchSpec}. In 86 * this case, the parents will be ranked based on an aggregation, such as the sum, of the signals 87 * calculated by scoring the joined documents with the advanced ranking strategy. 88 * 89 * <p>In terms of scoring, if {@link SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE} is set in 90 * {@link SearchSpec#getRankingStrategy}, the scores of the outer SearchResults can be influenced by 91 * the ranking signals of the subquery results. For example, if the {@link 92 * JoinSpec#getAggregationScoringStrategy} is set to: 93 * 94 * <ul> 95 * <li>{@link JoinSpec#AGGREGATION_SCORING_MIN_RANKING_SIGNAL}, the ranking signal of the outer 96 * {@link SearchResult} will be set to the minimum of the ranking signals of the subquery 97 * results. In this case, it will be the minimum of 2 and 3, which is 2. 98 * <li>{@link JoinSpec#AGGREGATION_SCORING_MAX_RANKING_SIGNAL}, the ranking signal of the outer 99 * {@link SearchResult} will be 3. 100 * <li>{@link JoinSpec#AGGREGATION_SCORING_AVG_RANKING_SIGNAL}, the ranking signal of the outer 101 * {@link SearchResult} will be 2.5. 102 * <li>{@link JoinSpec#AGGREGATION_SCORING_RESULT_COUNT}, the ranking signal of the outer {@link 103 * SearchResult} will be 2 as there are two joined results. 104 * <li>{@link JoinSpec#AGGREGATION_SCORING_SUM_RANKING_SIGNAL}, the ranking signal of the outer 105 * {@link SearchResult} will be 5, the sum of 2 and 3. 106 * <li>{@link JoinSpec#AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL}, the ranking signal of the 107 * outer {@link SearchResult} will stay as it is. 108 * </ul> 109 * 110 * <p>Referring to "this.childrenRankingSignals()" in the ranking signal of the outer query will 111 * return the signals calculated by scoring the joined documents using the scoring strategy in the 112 * nested {@link SearchSpec}, as in {@link SearchResult#getRankingSignal}. 113 */ 114 @SafeParcelable.Class(creator = "JoinSpecCreator") 115 @SuppressWarnings("HiddenSuperclass") 116 public final class JoinSpec extends AbstractSafeParcelable { 117 /** Creator class for {@link JoinSpec}. */ 118 @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) 119 @NonNull 120 public static final Parcelable.Creator<JoinSpec> CREATOR = new JoinSpecCreator(); 121 122 @Field(id = 1, getter = "getNestedQuery") 123 private final String mNestedQuery; 124 125 @Field(id = 2, getter = "getNestedSearchSpec") 126 private final SearchSpec mNestedSearchSpec; 127 128 @Field(id = 3, getter = "getChildPropertyExpression") 129 private final String mChildPropertyExpression; 130 131 @Field(id = 4, getter = "getMaxJoinedResultCount") 132 private final int mMaxJoinedResultCount; 133 134 @Field(id = 5, getter = "getAggregationScoringStrategy") 135 private final int mAggregationScoringStrategy; 136 137 private static final int DEFAULT_MAX_JOINED_RESULT_COUNT = 10; 138 139 /** 140 * A property expression referring to the combined package name, database name, namespace, and 141 * id of the document. 142 * 143 * <p>For instance, if a document with an id of "id1" exists in the namespace "ns" within the 144 * database "db" created by package "pkg", this would evaluate to "pkg$db/ns#id1". 145 * 146 * @hide 147 */ 148 public static final String QUALIFIED_ID = "this.qualifiedId()"; 149 150 /** 151 * Aggregation scoring strategy for join spec. 152 * 153 * @hide 154 */ 155 // NOTE: The integer values of these constants must match the proto enum constants in 156 // {@link JoinSpecProto.AggregationScoreStrategy.Code} 157 @IntDef( 158 value = { 159 AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL, 160 AGGREGATION_SCORING_RESULT_COUNT, 161 AGGREGATION_SCORING_MIN_RANKING_SIGNAL, 162 AGGREGATION_SCORING_AVG_RANKING_SIGNAL, 163 AGGREGATION_SCORING_MAX_RANKING_SIGNAL, 164 AGGREGATION_SCORING_SUM_RANKING_SIGNAL 165 }) 166 @Retention(RetentionPolicy.SOURCE) 167 public @interface AggregationScoringStrategy {} 168 169 /** 170 * Do not score the aggregation of joined documents. This is for the case where we want to 171 * perform a join, but keep the parent ranking signal. 172 */ 173 public static final int AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL = 0; 174 175 /** Score the aggregation of joined documents by counting the number of results. */ 176 public static final int AGGREGATION_SCORING_RESULT_COUNT = 1; 177 178 /** Score the aggregation of joined documents using the smallest ranking signal. */ 179 public static final int AGGREGATION_SCORING_MIN_RANKING_SIGNAL = 2; 180 181 /** Score the aggregation of joined documents using the average ranking signal. */ 182 public static final int AGGREGATION_SCORING_AVG_RANKING_SIGNAL = 3; 183 184 /** Score the aggregation of joined documents using the largest ranking signal. */ 185 public static final int AGGREGATION_SCORING_MAX_RANKING_SIGNAL = 4; 186 187 /** Score the aggregation of joined documents using the sum of ranking signal. */ 188 public static final int AGGREGATION_SCORING_SUM_RANKING_SIGNAL = 5; 189 190 @Constructor JoinSpec( @aramid = 1) @onNull String nestedQuery, @Param(id = 2) @NonNull SearchSpec nestedSearchSpec, @Param(id = 3) @NonNull String childPropertyExpression, @Param(id = 4) int maxJoinedResultCount, @Param(id = 5) @AggregationScoringStrategy int aggregationScoringStrategy)191 JoinSpec( 192 @Param(id = 1) @NonNull String nestedQuery, 193 @Param(id = 2) @NonNull SearchSpec nestedSearchSpec, 194 @Param(id = 3) @NonNull String childPropertyExpression, 195 @Param(id = 4) int maxJoinedResultCount, 196 @Param(id = 5) @AggregationScoringStrategy int aggregationScoringStrategy) { 197 mNestedQuery = Objects.requireNonNull(nestedQuery); 198 mNestedSearchSpec = Objects.requireNonNull(nestedSearchSpec); 199 mChildPropertyExpression = Objects.requireNonNull(childPropertyExpression); 200 mMaxJoinedResultCount = maxJoinedResultCount; 201 mAggregationScoringStrategy = aggregationScoringStrategy; 202 } 203 204 /** Returns the query to run on the joined documents. */ 205 @NonNull getNestedQuery()206 public String getNestedQuery() { 207 return mNestedQuery; 208 } 209 210 /** 211 * The property expression that is used to get values from child documents, returned from the 212 * nested search. These values are then used to match them to parent documents. These are 213 * analogous to foreign keys. 214 * 215 * @return the property expression to match in the child documents. 216 * @see Builder 217 */ 218 @NonNull getChildPropertyExpression()219 public String getChildPropertyExpression() { 220 return mChildPropertyExpression; 221 } 222 223 /** 224 * Returns the max amount of {@link SearchResult} objects to return with the parent document, 225 * with a default of 10 SearchResults. 226 */ getMaxJoinedResultCount()227 public int getMaxJoinedResultCount() { 228 return mMaxJoinedResultCount; 229 } 230 231 /** 232 * Returns the search spec used to retrieve the joined documents. 233 * 234 * <p>If {@link Builder#setNestedSearch} is never called, this will return a {@link SearchSpec} 235 * with all default values. This will match every document, as the nested search query will be 236 * "" and no schema will be filtered out. 237 */ 238 @NonNull getNestedSearchSpec()239 public SearchSpec getNestedSearchSpec() { 240 return mNestedSearchSpec; 241 } 242 243 /** 244 * Gets the joined document list scoring strategy. 245 * 246 * <p>The default scoring strategy is {@link #AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL}, 247 * which specifies that the score of the outer parent document will be used. 248 * 249 * @see SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE 250 */ 251 @AggregationScoringStrategy getAggregationScoringStrategy()252 public int getAggregationScoringStrategy() { 253 return mAggregationScoringStrategy; 254 } 255 256 @Override 257 @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) writeToParcel(@onNull Parcel dest, int flags)258 public void writeToParcel(@NonNull Parcel dest, int flags) { 259 JoinSpecCreator.writeToParcel(this, dest, flags); 260 } 261 262 /** Builder for {@link JoinSpec objects}. */ 263 public static final class Builder { 264 265 // The default nested SearchSpec. 266 private static final SearchSpec EMPTY_SEARCH_SPEC = new SearchSpec.Builder().build(); 267 268 private String mNestedQuery = ""; 269 private SearchSpec mNestedSearchSpec = EMPTY_SEARCH_SPEC; 270 private final String mChildPropertyExpression; 271 private int mMaxJoinedResultCount = DEFAULT_MAX_JOINED_RESULT_COUNT; 272 273 @AggregationScoringStrategy 274 private int mAggregationScoringStrategy = AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL; 275 276 /** 277 * Create a specification for the joining operation in search. 278 * 279 * <p>The child property expressions Specifies how to join documents. Documents with a child 280 * property expression equal to the qualified id of the parent will be retrieved. 281 * 282 * <p>Property expressions differ from {@link PropertyPath} as property expressions may 283 * refer to document properties or nested document properties such as "person.business.id" 284 * as well as a property expression. Currently the only property expression is 285 * "this.qualifiedId()". {@link PropertyPath} objects may only reference document properties 286 * and nested document properties. 287 * 288 * <p>In order to join a child document to a parent document, the child document must 289 * contain the parent's qualified id at the property expression specified by this method. 290 * 291 * @param childPropertyExpression the property to match in the child documents. 292 */ 293 // TODO(b/256022027): Reword comments to reference either "expression" or "PropertyPath" 294 // once wording is finalized. 295 // TODO(b/256022027): Add another method to allow providing PropertyPath objects as 296 // equality constraints. 297 // TODO(b/256022027): Change to allow for multiple child property expressions if multiple 298 // parent property expressions get supported. Builder(@onNull String childPropertyExpression)299 public Builder(@NonNull String childPropertyExpression) { 300 Objects.requireNonNull(childPropertyExpression); 301 mChildPropertyExpression = childPropertyExpression; 302 } 303 304 /** @hide */ Builder(@onNull JoinSpec joinSpec)305 public Builder(@NonNull JoinSpec joinSpec) { 306 Objects.requireNonNull(joinSpec); 307 mNestedQuery = joinSpec.getNestedQuery(); 308 mNestedSearchSpec = joinSpec.getNestedSearchSpec(); 309 mChildPropertyExpression = joinSpec.getChildPropertyExpression(); 310 mMaxJoinedResultCount = joinSpec.getMaxJoinedResultCount(); 311 mAggregationScoringStrategy = joinSpec.getAggregationScoringStrategy(); 312 } 313 314 /** 315 * Sets the query and the SearchSpec for the documents being joined. This will score and 316 * rank the joined documents as well as filter the joined documents. 317 * 318 * <p>If {@link SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE} is set in the outer {@link 319 * SearchSpec}, the resulting signals will be used to rank the parent documents. Note that 320 * the aggregation strategy also needs to be set with {@link 321 * JoinSpec.Builder#setAggregationScoringStrategy}, otherwise the default will be {@link 322 * JoinSpec#AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL}, which will just use the parent 323 * documents ranking signal. 324 * 325 * <p>If this method is never called, {@link JoinSpec#getNestedQuery} will return an empty 326 * string, meaning we will join with every possible document that matches the equality 327 * constraints and hasn't been filtered out by the type or namespace filters. 328 * 329 * @see JoinSpec#getNestedQuery 330 * @see JoinSpec#getNestedSearchSpec 331 */ 332 @SuppressWarnings("MissingGetterMatchingBuilder") 333 // See getNestedQuery & getNestedSearchSpec 334 @CanIgnoreReturnValue 335 @NonNull setNestedSearch( @onNull String nestedQuery, @NonNull SearchSpec nestedSearchSpec)336 public Builder setNestedSearch( 337 @NonNull String nestedQuery, @NonNull SearchSpec nestedSearchSpec) { 338 Objects.requireNonNull(nestedQuery); 339 Objects.requireNonNull(nestedSearchSpec); 340 mNestedQuery = nestedQuery; 341 mNestedSearchSpec = nestedSearchSpec; 342 343 return this; 344 } 345 346 /** 347 * Sets the max amount of {@link SearchResults} to return with the parent document, with a 348 * default of 10 SearchResults. 349 * 350 * <p>This does NOT limit the number of results that are joined with the parent document for 351 * scoring. This means that, when set, only a maximum of {@code maxJoinedResultCount} 352 * results will be returned with each parent document, but all results that are joined with 353 * a parent will factor into the score. 354 */ 355 @CanIgnoreReturnValue 356 @NonNull setMaxJoinedResultCount(int maxJoinedResultCount)357 public Builder setMaxJoinedResultCount(int maxJoinedResultCount) { 358 mMaxJoinedResultCount = maxJoinedResultCount; 359 return this; 360 } 361 362 /** 363 * Sets how we derive a single score from a list of joined documents. 364 * 365 * <p>The default scoring strategy is {@link 366 * #AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL}, which specifies that the ranking 367 * signal of the outer parent document will be used. 368 * 369 * @see SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE 370 */ 371 @CanIgnoreReturnValue 372 @NonNull setAggregationScoringStrategy( @ggregationScoringStrategy int aggregationScoringStrategy)373 public Builder setAggregationScoringStrategy( 374 @AggregationScoringStrategy int aggregationScoringStrategy) { 375 Preconditions.checkArgumentInRange( 376 aggregationScoringStrategy, 377 AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL, 378 AGGREGATION_SCORING_SUM_RANKING_SIGNAL, 379 "aggregationScoringStrategy"); 380 mAggregationScoringStrategy = aggregationScoringStrategy; 381 return this; 382 } 383 384 /** Constructs a new {@link JoinSpec} from the contents of this builder. */ 385 @NonNull build()386 public JoinSpec build() { 387 return new JoinSpec( 388 mNestedQuery, 389 mNestedSearchSpec, 390 mChildPropertyExpression, 391 mMaxJoinedResultCount, 392 mAggregationScoringStrategy); 393 } 394 } 395 } 396