| /* |
| * Copyright 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.app.appsearch; |
| |
| import android.annotation.FlaggedApi; |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.app.appsearch.annotation.CanIgnoreReturnValue; |
| import android.app.appsearch.flags.Flags; |
| import android.app.appsearch.safeparcel.AbstractSafeParcelable; |
| import android.app.appsearch.safeparcel.SafeParcelable; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| |
| import com.android.internal.util.Preconditions; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.Objects; |
| |
| /** |
| * This class represents the specifications for the joining operation in search. |
| * |
| * <p>Joins are only possible for matching on the qualified id of an outer document and a property |
| * value within a subquery document. In the subquery documents, these values may be referred to with |
| * a property path such as "email.recipient.id" or "entityId" or a property expression. One such |
| * property expression is "this.qualifiedId()", which refers to the document's combined package, |
| * database, namespace, and id. |
| * |
| * <p>Note that in order for perform the join, the property referred to by {@link |
| * #getChildPropertyExpression} has to be a property with {@link |
| * AppSearchSchema.StringPropertyConfig#getJoinableValueType} set to {@link |
| * AppSearchSchema.StringPropertyConfig#JOINABLE_VALUE_TYPE_QUALIFIED_ID}. Otherwise no documents |
| * will be joined to any {@link SearchResult}. |
| * |
| * <p>Take these outer query and subquery results for example: |
| * |
| * <pre>{@code |
| * Outer result { |
| * id: id1 |
| * score: 5 |
| * } |
| * Subquery result 1 { |
| * id: id2 |
| * score: 2 |
| * entityId: pkg$db/ns#id1 |
| * notes: This is some doc |
| * } |
| * Subquery result 2 { |
| * id: id3 |
| * score: 3 |
| * entityId: pkg$db/ns#id2 |
| * notes: This is another doc |
| * } |
| * }</pre> |
| * |
| * <p>In this example, subquery result 1 contains a property "entityId" whose value is |
| * "pkg$db/ns#id1", referring to the outer result. If you call {@link Builder} with "entityId", we |
| * will retrieve the value of the property "entityId" from the child document, which is |
| * "pkg$db#ns/id1". Let's say the qualified id of the outer result is "pkg$db#ns/id1". This would |
| * mean the subquery result 1 document will be matched to that parent document. This is done by |
| * adding a {@link SearchResult} containing the child document to the top-level parent {@link |
| * SearchResult#getJoinedResults}. |
| * |
| * <p>If {@link #getChildPropertyExpression} is "notes", we will check the values of the notes |
| * property in the subquery results. In subquery result 1, this values is "This is some doc", which |
| * does not equal the qualified id of the outer query result. As such, subquery result 1 will not be |
| * joined to the outer query result. |
| * |
| * <p>It's possible to define an advanced ranking strategy in the nested {@link SearchSpec} and also |
| * use {@link SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE} in the outer {@link SearchSpec}. In |
| * this case, the parents will be ranked based on an aggregation, such as the sum, of the signals |
| * calculated by scoring the joined documents with the advanced ranking strategy. |
| * |
| * <p>In terms of scoring, if {@link SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE} is set in |
| * {@link SearchSpec#getRankingStrategy}, the scores of the outer SearchResults can be influenced by |
| * the ranking signals of the subquery results. For example, if the {@link |
| * JoinSpec#getAggregationScoringStrategy} is set to: |
| * |
| * <ul> |
| * <li>{@link JoinSpec#AGGREGATION_SCORING_MIN_RANKING_SIGNAL}, the ranking signal of the outer |
| * {@link SearchResult} will be set to the minimum of the ranking signals of the subquery |
| * results. In this case, it will be the minimum of 2 and 3, which is 2. |
| * <li>{@link JoinSpec#AGGREGATION_SCORING_MAX_RANKING_SIGNAL}, the ranking signal of the outer |
| * {@link SearchResult} will be 3. |
| * <li>{@link JoinSpec#AGGREGATION_SCORING_AVG_RANKING_SIGNAL}, the ranking signal of the outer |
| * {@link SearchResult} will be 2.5. |
| * <li>{@link JoinSpec#AGGREGATION_SCORING_RESULT_COUNT}, the ranking signal of the outer {@link |
| * SearchResult} will be 2 as there are two joined results. |
| * <li>{@link JoinSpec#AGGREGATION_SCORING_SUM_RANKING_SIGNAL}, the ranking signal of the outer |
| * {@link SearchResult} will be 5, the sum of 2 and 3. |
| * <li>{@link JoinSpec#AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL}, the ranking signal of the |
| * outer {@link SearchResult} will stay as it is. |
| * </ul> |
| * |
| * <p>Referring to "this.childrenRankingSignals()" in the ranking signal of the outer query will |
| * return the signals calculated by scoring the joined documents using the scoring strategy in the |
| * nested {@link SearchSpec}, as in {@link SearchResult#getRankingSignal}. |
| */ |
| @SafeParcelable.Class(creator = "JoinSpecCreator") |
| @SuppressWarnings("HiddenSuperclass") |
| public final class JoinSpec extends AbstractSafeParcelable { |
| /** Creator class for {@link JoinSpec}. */ |
| @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) |
| @NonNull |
| public static final Parcelable.Creator<JoinSpec> CREATOR = new JoinSpecCreator(); |
| |
| @Field(id = 1, getter = "getNestedQuery") |
| private final String mNestedQuery; |
| |
| @Field(id = 2, getter = "getNestedSearchSpec") |
| private final SearchSpec mNestedSearchSpec; |
| |
| @Field(id = 3, getter = "getChildPropertyExpression") |
| private final String mChildPropertyExpression; |
| |
| @Field(id = 4, getter = "getMaxJoinedResultCount") |
| private final int mMaxJoinedResultCount; |
| |
| @Field(id = 5, getter = "getAggregationScoringStrategy") |
| private final int mAggregationScoringStrategy; |
| |
| private static final int DEFAULT_MAX_JOINED_RESULT_COUNT = 10; |
| |
| /** |
| * A property expression referring to the combined package name, database name, namespace, and |
| * id of the document. |
| * |
| * <p>For instance, if a document with an id of "id1" exists in the namespace "ns" within the |
| * database "db" created by package "pkg", this would evaluate to "pkg$db/ns#id1". |
| * |
| * @hide |
| */ |
| public static final String QUALIFIED_ID = "this.qualifiedId()"; |
| |
| /** |
| * Aggregation scoring strategy for join spec. |
| * |
| * @hide |
| */ |
| // NOTE: The integer values of these constants must match the proto enum constants in |
| // {@link JoinSpecProto.AggregationScoreStrategy.Code} |
| @IntDef( |
| value = { |
| AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL, |
| AGGREGATION_SCORING_RESULT_COUNT, |
| AGGREGATION_SCORING_MIN_RANKING_SIGNAL, |
| AGGREGATION_SCORING_AVG_RANKING_SIGNAL, |
| AGGREGATION_SCORING_MAX_RANKING_SIGNAL, |
| AGGREGATION_SCORING_SUM_RANKING_SIGNAL |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface AggregationScoringStrategy {} |
| |
| /** |
| * Do not score the aggregation of joined documents. This is for the case where we want to |
| * perform a join, but keep the parent ranking signal. |
| */ |
| public static final int AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL = 0; |
| |
| /** Score the aggregation of joined documents by counting the number of results. */ |
| public static final int AGGREGATION_SCORING_RESULT_COUNT = 1; |
| |
| /** Score the aggregation of joined documents using the smallest ranking signal. */ |
| public static final int AGGREGATION_SCORING_MIN_RANKING_SIGNAL = 2; |
| |
| /** Score the aggregation of joined documents using the average ranking signal. */ |
| public static final int AGGREGATION_SCORING_AVG_RANKING_SIGNAL = 3; |
| |
| /** Score the aggregation of joined documents using the largest ranking signal. */ |
| public static final int AGGREGATION_SCORING_MAX_RANKING_SIGNAL = 4; |
| |
| /** Score the aggregation of joined documents using the sum of ranking signal. */ |
| public static final int AGGREGATION_SCORING_SUM_RANKING_SIGNAL = 5; |
| |
| @Constructor |
| JoinSpec( |
| @Param(id = 1) @NonNull 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) { |
| mNestedQuery = Objects.requireNonNull(nestedQuery); |
| mNestedSearchSpec = Objects.requireNonNull(nestedSearchSpec); |
| mChildPropertyExpression = Objects.requireNonNull(childPropertyExpression); |
| mMaxJoinedResultCount = maxJoinedResultCount; |
| mAggregationScoringStrategy = aggregationScoringStrategy; |
| } |
| |
| /** Returns the query to run on the joined documents. */ |
| @NonNull |
| public String getNestedQuery() { |
| return mNestedQuery; |
| } |
| |
| /** |
| * The property expression that is used to get values from child documents, returned from the |
| * nested search. These values are then used to match them to parent documents. These are |
| * analogous to foreign keys. |
| * |
| * @return the property expression to match in the child documents. |
| * @see Builder |
| */ |
| @NonNull |
| public String getChildPropertyExpression() { |
| return mChildPropertyExpression; |
| } |
| |
| /** |
| * Returns the max amount of {@link SearchResult} objects to return with the parent document, |
| * with a default of 10 SearchResults. |
| */ |
| public int getMaxJoinedResultCount() { |
| return mMaxJoinedResultCount; |
| } |
| |
| /** |
| * Returns the search spec used to retrieve the joined documents. |
| * |
| * <p>If {@link Builder#setNestedSearch} is never called, this will return a {@link SearchSpec} |
| * with all default values. This will match every document, as the nested search query will be |
| * "" and no schema will be filtered out. |
| */ |
| @NonNull |
| public SearchSpec getNestedSearchSpec() { |
| return mNestedSearchSpec; |
| } |
| |
| /** |
| * Gets the joined document list scoring strategy. |
| * |
| * <p>The default scoring strategy is {@link #AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL}, |
| * which specifies that the score of the outer parent document will be used. |
| * |
| * @see SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE |
| */ |
| @AggregationScoringStrategy |
| public int getAggregationScoringStrategy() { |
| return mAggregationScoringStrategy; |
| } |
| |
| @Override |
| @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) |
| public void writeToParcel(@NonNull Parcel dest, int flags) { |
| JoinSpecCreator.writeToParcel(this, dest, flags); |
| } |
| |
| /** Builder for {@link JoinSpec objects}. */ |
| public static final class Builder { |
| |
| // The default nested SearchSpec. |
| private static final SearchSpec EMPTY_SEARCH_SPEC = new SearchSpec.Builder().build(); |
| |
| private String mNestedQuery = ""; |
| private SearchSpec mNestedSearchSpec = EMPTY_SEARCH_SPEC; |
| private final String mChildPropertyExpression; |
| private int mMaxJoinedResultCount = DEFAULT_MAX_JOINED_RESULT_COUNT; |
| |
| @AggregationScoringStrategy |
| private int mAggregationScoringStrategy = AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL; |
| |
| /** |
| * Create a specification for the joining operation in search. |
| * |
| * <p>The child property expressions Specifies how to join documents. Documents with a child |
| * property expression equal to the qualified id of the parent will be retrieved. |
| * |
| * <p>Property expressions differ from {@link PropertyPath} as property expressions may |
| * refer to document properties or nested document properties such as "person.business.id" |
| * as well as a property expression. Currently the only property expression is |
| * "this.qualifiedId()". {@link PropertyPath} objects may only reference document properties |
| * and nested document properties. |
| * |
| * <p>In order to join a child document to a parent document, the child document must |
| * contain the parent's qualified id at the property expression specified by this method. |
| * |
| * @param childPropertyExpression the property to match in the child documents. |
| */ |
| // TODO(b/256022027): Reword comments to reference either "expression" or "PropertyPath" |
| // once wording is finalized. |
| // TODO(b/256022027): Add another method to allow providing PropertyPath objects as |
| // equality constraints. |
| // TODO(b/256022027): Change to allow for multiple child property expressions if multiple |
| // parent property expressions get supported. |
| public Builder(@NonNull String childPropertyExpression) { |
| Objects.requireNonNull(childPropertyExpression); |
| mChildPropertyExpression = childPropertyExpression; |
| } |
| |
| /** @hide */ |
| public Builder(@NonNull JoinSpec joinSpec) { |
| Objects.requireNonNull(joinSpec); |
| mNestedQuery = joinSpec.getNestedQuery(); |
| mNestedSearchSpec = joinSpec.getNestedSearchSpec(); |
| mChildPropertyExpression = joinSpec.getChildPropertyExpression(); |
| mMaxJoinedResultCount = joinSpec.getMaxJoinedResultCount(); |
| mAggregationScoringStrategy = joinSpec.getAggregationScoringStrategy(); |
| } |
| |
| /** |
| * Sets the query and the SearchSpec for the documents being joined. This will score and |
| * rank the joined documents as well as filter the joined documents. |
| * |
| * <p>If {@link SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE} is set in the outer {@link |
| * SearchSpec}, the resulting signals will be used to rank the parent documents. Note that |
| * the aggregation strategy also needs to be set with {@link |
| * JoinSpec.Builder#setAggregationScoringStrategy}, otherwise the default will be {@link |
| * JoinSpec#AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL}, which will just use the parent |
| * documents ranking signal. |
| * |
| * <p>If this method is never called, {@link JoinSpec#getNestedQuery} will return an empty |
| * string, meaning we will join with every possible document that matches the equality |
| * constraints and hasn't been filtered out by the type or namespace filters. |
| * |
| * @see JoinSpec#getNestedQuery |
| * @see JoinSpec#getNestedSearchSpec |
| */ |
| @SuppressWarnings("MissingGetterMatchingBuilder") |
| // See getNestedQuery & getNestedSearchSpec |
| @CanIgnoreReturnValue |
| @NonNull |
| public Builder setNestedSearch( |
| @NonNull String nestedQuery, @NonNull SearchSpec nestedSearchSpec) { |
| Objects.requireNonNull(nestedQuery); |
| Objects.requireNonNull(nestedSearchSpec); |
| mNestedQuery = nestedQuery; |
| mNestedSearchSpec = nestedSearchSpec; |
| |
| return this; |
| } |
| |
| /** |
| * Sets the max amount of {@link SearchResults} to return with the parent document, with a |
| * default of 10 SearchResults. |
| * |
| * <p>This does NOT limit the number of results that are joined with the parent document for |
| * scoring. This means that, when set, only a maximum of {@code maxJoinedResultCount} |
| * results will be returned with each parent document, but all results that are joined with |
| * a parent will factor into the score. |
| */ |
| @CanIgnoreReturnValue |
| @NonNull |
| public Builder setMaxJoinedResultCount(int maxJoinedResultCount) { |
| mMaxJoinedResultCount = maxJoinedResultCount; |
| return this; |
| } |
| |
| /** |
| * Sets how we derive a single score from a list of joined documents. |
| * |
| * <p>The default scoring strategy is {@link |
| * #AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL}, which specifies that the ranking |
| * signal of the outer parent document will be used. |
| * |
| * @see SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE |
| */ |
| @CanIgnoreReturnValue |
| @NonNull |
| public Builder setAggregationScoringStrategy( |
| @AggregationScoringStrategy int aggregationScoringStrategy) { |
| Preconditions.checkArgumentInRange( |
| aggregationScoringStrategy, |
| AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL, |
| AGGREGATION_SCORING_SUM_RANKING_SIGNAL, |
| "aggregationScoringStrategy"); |
| mAggregationScoringStrategy = aggregationScoringStrategy; |
| return this; |
| } |
| |
| /** Constructs a new {@link JoinSpec} from the contents of this builder. */ |
| @NonNull |
| public JoinSpec build() { |
| return new JoinSpec( |
| mNestedQuery, |
| mNestedSearchSpec, |
| mChildPropertyExpression, |
| mMaxJoinedResultCount, |
| mAggregationScoringStrategy); |
| } |
| } |
| } |