• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 // TODO(b/384721898): Switch to JSpecify annotations
116 @SuppressWarnings({"HiddenSuperclass", "JSpecifyNullness"})
117 public final class JoinSpec extends AbstractSafeParcelable {
118     /** Creator class for {@link JoinSpec}. */
119     @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
120     public static final @NonNull 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. */
getNestedQuery()205     public @NonNull String getNestedQuery() {
206         return mNestedQuery;
207     }
208 
209     /**
210      * The property expression that is used to get values from child documents, returned from the
211      * nested search. These values are then used to match them to parent documents. These are
212      * analogous to foreign keys.
213      *
214      * @return the property expression to match in the child documents.
215      * @see Builder
216      */
getChildPropertyExpression()217     public @NonNull String getChildPropertyExpression() {
218         return mChildPropertyExpression;
219     }
220 
221     /**
222      * Returns the max amount of {@link SearchResult} objects to return with the parent document,
223      * with a default of 10 SearchResults.
224      */
getMaxJoinedResultCount()225     public int getMaxJoinedResultCount() {
226         return mMaxJoinedResultCount;
227     }
228 
229     /**
230      * Returns the search spec used to retrieve the joined documents.
231      *
232      * <p>If {@link Builder#setNestedSearch} is never called, this will return a {@link SearchSpec}
233      * with all default values. This will match every document, as the nested search query will be
234      * "" and no schema will be filtered out.
235      */
getNestedSearchSpec()236     public @NonNull SearchSpec getNestedSearchSpec() {
237         return mNestedSearchSpec;
238     }
239 
240     /**
241      * Gets the joined document list scoring strategy.
242      *
243      * <p>The default scoring strategy is {@link #AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL},
244      * which specifies that the score of the outer parent document will be used.
245      *
246      * @see SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE
247      */
248     @AggregationScoringStrategy
getAggregationScoringStrategy()249     public int getAggregationScoringStrategy() {
250         return mAggregationScoringStrategy;
251     }
252 
253     @Override
254     @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2)
writeToParcel(@onNull Parcel dest, int flags)255     public void writeToParcel(@NonNull Parcel dest, int flags) {
256         JoinSpecCreator.writeToParcel(this, dest, flags);
257     }
258 
259     /** Builder for {@link JoinSpec objects}. */
260     public static final class Builder {
261 
262         // The default nested SearchSpec.
263         private static final SearchSpec EMPTY_SEARCH_SPEC = new SearchSpec.Builder().build();
264 
265         private String mNestedQuery = "";
266         private SearchSpec mNestedSearchSpec = EMPTY_SEARCH_SPEC;
267         private String mChildPropertyExpression;
268         private int mMaxJoinedResultCount = DEFAULT_MAX_JOINED_RESULT_COUNT;
269 
270         @AggregationScoringStrategy
271         private int mAggregationScoringStrategy = AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL;
272 
273         /**
274          * Create a specification for the joining operation in search.
275          *
276          * <p>The child property expression specifies how to join documents. Documents with a child
277          * property expression equal to the qualified id of the parent will be retrieved.
278          *
279          * <p>Property expressions differ from {@link PropertyPath} as property expressions may
280          * refer to document properties or nested document properties such as "person.business.id"
281          * as well as a property expression. Currently the only property expression is
282          * "this.qualifiedId()". {@link PropertyPath} objects may only reference document properties
283          * and nested document properties.
284          *
285          * <p>In order to join a child document to a parent document, the child document must
286          * contain the parent's qualified id at the property expression specified by this method.
287          *
288          * @param childPropertyExpression the property to match in the child documents.
289          */
290         // TODO(b/256022027): Reword comments to reference either "expression" or "PropertyPath"
291         //  once wording is finalized.
292         // TODO(b/256022027): Add another method to allow providing PropertyPath objects as
293         //  equality constraints.
294         // TODO(b/256022027): Change to allow for multiple child property expressions if multiple
295         //  parent property expressions get supported.
Builder(@onNull String childPropertyExpression)296         public Builder(@NonNull String childPropertyExpression) {
297             Objects.requireNonNull(childPropertyExpression);
298             mChildPropertyExpression = childPropertyExpression;
299         }
300 
301         /** Creates a new {@link Builder} from the given {@link JoinSpec}. */
302         @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
Builder(@onNull JoinSpec joinSpec)303         public Builder(@NonNull JoinSpec joinSpec) {
304             Objects.requireNonNull(joinSpec);
305             mNestedQuery = joinSpec.getNestedQuery();
306             mNestedSearchSpec = joinSpec.getNestedSearchSpec();
307             mChildPropertyExpression = joinSpec.getChildPropertyExpression();
308             mMaxJoinedResultCount = joinSpec.getMaxJoinedResultCount();
309             mAggregationScoringStrategy = joinSpec.getAggregationScoringStrategy();
310         }
311 
312         /**
313          * Sets the child property expression.
314          *
315          * <p>The child property expression specifies how to join documents. Documents with a child
316          * property expression equal to the qualified id of the parent will be retrieved.
317          *
318          * <p>Property expressions differ from {@link PropertyPath} as property expressions may
319          * refer to document properties or nested document properties such as "person.business.id"
320          * as well as a property expression. Currently the only property expression is
321          * "this.qualifiedId()". {@link PropertyPath} objects may only reference document properties
322          * and nested document properties.
323          *
324          * <p>In order to join a child document to a parent document, the child document must
325          * contain the parent's qualified id at the property expression specified by this method.
326          *
327          * @param childPropertyExpression the property to match in the child documents.
328          */
329         @FlaggedApi(Flags.FLAG_ENABLE_ADDITIONAL_BUILDER_COPY_CONSTRUCTORS)
330         @CanIgnoreReturnValue
setChildPropertyExpression( @onNull String childPropertyExpression)331         public @NonNull Builder setChildPropertyExpression(
332                 @NonNull String childPropertyExpression) {
333             Objects.requireNonNull(childPropertyExpression);
334             mChildPropertyExpression = childPropertyExpression;
335             return this;
336         }
337 
338         /**
339          * Sets the query and the SearchSpec for the documents being joined. This will score and
340          * rank the joined documents as well as filter the joined documents.
341          *
342          * <p>If {@link SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE} is set in the outer {@link
343          * SearchSpec}, the resulting signals will be used to rank the parent documents. Note that
344          * the aggregation strategy also needs to be set with {@link
345          * JoinSpec.Builder#setAggregationScoringStrategy}, otherwise the default will be {@link
346          * JoinSpec#AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL}, which will just use the parent
347          * documents ranking signal.
348          *
349          * <p>If this method is never called, {@link JoinSpec#getNestedQuery} will return an empty
350          * string, meaning we will join with every possible document that matches the equality
351          * constraints and hasn't been filtered out by the type or namespace filters.
352          *
353          * @see JoinSpec#getNestedQuery
354          * @see JoinSpec#getNestedSearchSpec
355          */
356         @SuppressWarnings("MissingGetterMatchingBuilder")
357         // See getNestedQuery & getNestedSearchSpec
358         @CanIgnoreReturnValue
setNestedSearch( @onNull String nestedQuery, @NonNull SearchSpec nestedSearchSpec)359         public @NonNull Builder setNestedSearch(
360                 @NonNull String nestedQuery, @NonNull SearchSpec nestedSearchSpec) {
361             Objects.requireNonNull(nestedQuery);
362             Objects.requireNonNull(nestedSearchSpec);
363             mNestedQuery = nestedQuery;
364             mNestedSearchSpec = nestedSearchSpec;
365             return this;
366         }
367 
368         /**
369          * Sets the max amount of {@link SearchResults} to return with the parent document, with a
370          * default of 10 SearchResults.
371          *
372          * <p>This does NOT limit the number of results that are joined with the parent document for
373          * scoring. This means that, when set, only a maximum of {@code maxJoinedResultCount}
374          * results will be returned with each parent document, but all results that are joined with
375          * a parent will factor into the score.
376          */
377         @CanIgnoreReturnValue
setMaxJoinedResultCount(int maxJoinedResultCount)378         public @NonNull Builder setMaxJoinedResultCount(int maxJoinedResultCount) {
379             mMaxJoinedResultCount = maxJoinedResultCount;
380             return this;
381         }
382 
383         /**
384          * Sets how we derive a single score from a list of joined documents.
385          *
386          * <p>The default scoring strategy is {@link
387          * #AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL}, which specifies that the ranking
388          * signal of the outer parent document will be used.
389          *
390          * @see SearchSpec#RANKING_STRATEGY_JOIN_AGGREGATE_SCORE
391          */
392         @CanIgnoreReturnValue
setAggregationScoringStrategy( @ggregationScoringStrategy int aggregationScoringStrategy)393         public @NonNull Builder setAggregationScoringStrategy(
394                 @AggregationScoringStrategy int aggregationScoringStrategy) {
395             Preconditions.checkArgumentInRange(
396                     aggregationScoringStrategy,
397                     AGGREGATION_SCORING_OUTER_RESULT_RANKING_SIGNAL,
398                     AGGREGATION_SCORING_SUM_RANKING_SIGNAL,
399                     "aggregationScoringStrategy");
400             mAggregationScoringStrategy = aggregationScoringStrategy;
401             return this;
402         }
403 
404         /** Constructs a new {@link JoinSpec} from the contents of this builder. */
build()405         public @NonNull JoinSpec build() {
406             return new JoinSpec(
407                     mNestedQuery,
408                     mNestedSearchSpec,
409                     mChildPropertyExpression,
410                     mMaxJoinedResultCount,
411                     mAggregationScoringStrategy);
412         }
413     }
414 }
415