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