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 android.app.appsearch;
18 
19 
20 import android.annotation.FlaggedApi;
21 import android.annotation.NonNull;
22 import android.app.appsearch.annotation.CanIgnoreReturnValue;
23 import android.app.appsearch.exceptions.AppSearchException;
24 import android.util.ArraySet;
25 
26 import com.android.appsearch.flags.Flags;
27 
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Collection;
31 import java.util.Collections;
32 import java.util.List;
33 import java.util.Objects;
34 import java.util.Set;
35 
36 /**
37  * Encapsulates a request to index documents into an {@link AppSearchSession} database.
38  *
39  * @see AppSearchSession#put
40  */
41 public final class PutDocumentsRequest {
42     private final List<GenericDocument> mDocuments;
43 
44     private final List<GenericDocument> mTakenActions;
45 
PutDocumentsRequest(List<GenericDocument> documents, List<GenericDocument> takenActions)46     PutDocumentsRequest(List<GenericDocument> documents, List<GenericDocument> takenActions) {
47         mDocuments = documents;
48         mTakenActions = takenActions;
49     }
50 
51     /** Returns a list of {@link GenericDocument} objects that are part of this request. */
52     @NonNull
getGenericDocuments()53     public List<GenericDocument> getGenericDocuments() {
54         return Collections.unmodifiableList(mDocuments);
55     }
56 
57     /**
58      * Returns a list of {@link GenericDocument} objects containing taken action metrics that are
59      * part of this request.
60      *
61      * <p>See {@link Builder#addTakenActionGenericDocuments(GenericDocument...)}.
62      */
63     @NonNull
64     @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS)
getTakenActionGenericDocuments()65     public List<GenericDocument> getTakenActionGenericDocuments() {
66         return Collections.unmodifiableList(mTakenActions);
67     }
68 
69     /** Builder for {@link PutDocumentsRequest} objects. */
70     public static final class Builder {
71         private ArrayList<GenericDocument> mDocuments = new ArrayList<>();
72         private ArrayList<GenericDocument> mTakenActions = new ArrayList<>();
73         private boolean mBuilt = false;
74 
75         /** Adds one or more {@link GenericDocument} objects to the request. */
76         @CanIgnoreReturnValue
77         @NonNull
addGenericDocuments(@onNull GenericDocument... documents)78         public Builder addGenericDocuments(@NonNull GenericDocument... documents) {
79             Objects.requireNonNull(documents);
80             resetIfBuilt();
81             return addGenericDocuments(Arrays.asList(documents));
82         }
83 
84         /** Adds a collection of {@link GenericDocument} objects to the request. */
85         @CanIgnoreReturnValue
86         @NonNull
addGenericDocuments( @onNull Collection<? extends GenericDocument> documents)87         public Builder addGenericDocuments(
88                 @NonNull Collection<? extends GenericDocument> documents) {
89             Objects.requireNonNull(documents);
90             resetIfBuilt();
91             mDocuments.addAll(documents);
92             return this;
93         }
94 
95         /**
96          * Adds one or more {@link GenericDocument} objects containing taken action metrics to the
97          * request.
98          *
99          * <p>It is recommended to use taken action document classes in Jetpack library to construct
100          * taken action documents.
101          *
102          * <p>The document creation timestamp of the {@link GenericDocument} should be set to the
103          * actual action timestamp via {@link GenericDocument.Builder#setCreationTimestampMillis}.
104          *
105          * <p>Clients should report search and click actions together sorted by {@link
106          * GenericDocument#getCreationTimestampMillis} in chronological order.
107          *
108          * <p>For example, if there are 2 search actions, with 1 click action associated with the
109          * first and 2 click actions associated with the second, then clients should report
110          * [searchAction1, clickAction1, searchAction2, clickAction2, clickAction3].
111          *
112          * <p>Different types of taken actions and metrics to be collected by AppSearch:
113          *
114          * <ul>
115          *   <li>Search action
116          *       <ul>
117          *         <li>actionType: LONG, the enum value of the action type.
118          *             <p>Requires to be {@code 1} for search actions.
119          *         <li>query: STRING, the user-entered search input (without any operators or
120          *             rewriting).
121          *         <li>fetchedResultCount: LONG, the number of {@link SearchResult} documents
122          *             fetched from AppSearch in this search action.
123          *       </ul>
124          *   <li>Click action
125          *       <ul>
126          *         <li>actionType: LONG, the enum value of the action type.
127          *             <p>Requires to be {@code 2} for click actions.
128          *         <li>query: STRING, the user-entered search input (without any operators or
129          *             rewriting) that yielded the {@link SearchResult} on which the user took
130          *             action.
131          *         <li>referencedQualifiedId: STRING, the qualified id of the {@link SearchResult}
132          *             document that the user takes action on.
133          *             <p>A qualified id is a string generated by package, database, namespace, and
134          *             document id. See {@link
135          *             android.app.appsearch.util.DocumentIdUtil#createQualifiedId} for more
136          *             details.
137          *         <li>resultRankInBlock: LONG, the rank of the {@link SearchResult} document among
138          *             the user-defined block.
139          *             <p>The client can define its own custom definition for block, for example,
140          *             corpus name, group, etc.
141          *             <p>For example, a client defines the block as corpus, and AppSearch returns 5
142          *             documents with corpus = ["corpus1", "corpus1", "corpus2", "corpus3",
143          *             "corpus2"]. Then the block ranks of them = [1, 2, 1, 1, 2].
144          *             <p>If the client is not presenting the results in multiple blocks, they
145          *             should set this value to match resultRankGlobal.
146          *         <li>resultRankGlobal: LONG, the global rank of the {@link SearchResult} document.
147          *             <p>Global rank reflects the order of {@link SearchResult} documents returned
148          *             by AppSearch.
149          *             <p>For example, AppSearch returns 2 pages with 10 {@link SearchResult}
150          *             documents for each page. Then the global ranks of them will be 1 to 10 for
151          *             the first page, and 11 to 20 for the second page.
152          *         <li>timeStayOnResultMillis: LONG, the time in milliseconds that user stays on the
153          *             {@link SearchResult} document after clicking it.
154          *       </ul>
155          * </ul>
156          *
157          * <p>Certain anonymized information about actions reported using this API may be uploaded
158          * using statsd and may be used to improve the quality of the search algorithms. Most of the
159          * information in this class is already non-identifiable, such as durations and its position
160          * in the result set. Identifiable information which you choose to provide, such as the
161          * query string, will be anonymized using techniques like Federated Analytics to ensure only
162          * the most frequently searched terms across the whole user population are retained and
163          * available for study.
164          *
165          * <p>You can alternatively use the {@link #addGenericDocuments(GenericDocument...)} API to
166          * retain the benefits of joining and using it on-device, without triggering any of the
167          * anonymized stats uploading described above.
168          *
169          * @param takenActionGenericDocuments one or more {@link GenericDocument} objects containing
170          *     taken action metric fields.
171          */
172         @CanIgnoreReturnValue
173         @NonNull
174         @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS)
addTakenActionGenericDocuments( @onNull GenericDocument... takenActionGenericDocuments)175         public Builder addTakenActionGenericDocuments(
176                 @NonNull GenericDocument... takenActionGenericDocuments) throws AppSearchException {
177             Objects.requireNonNull(takenActionGenericDocuments);
178             resetIfBuilt();
179             return addTakenActionGenericDocuments(Arrays.asList(takenActionGenericDocuments));
180         }
181 
182         /**
183          * Adds a collection of {@link GenericDocument} objects containing taken action metrics to
184          * the request.
185          *
186          * @see #addTakenActionGenericDocuments(GenericDocument...)
187          * @param takenActionGenericDocuments a collection of {@link GenericDocument} objects
188          *     containing taken action metric fields.
189          */
190         @CanIgnoreReturnValue
191         @NonNull
192         @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS)
addTakenActionGenericDocuments( @onNull Collection<? extends GenericDocument> takenActionGenericDocuments)193         public Builder addTakenActionGenericDocuments(
194                 @NonNull Collection<? extends GenericDocument> takenActionGenericDocuments)
195                 throws AppSearchException {
196             Objects.requireNonNull(takenActionGenericDocuments);
197             resetIfBuilt();
198             mTakenActions.addAll(takenActionGenericDocuments);
199             return this;
200         }
201 
202         /**
203          * Creates a new {@link PutDocumentsRequest} object.
204          *
205          * @throws IllegalArgumentException if there is any id collision between normal and action
206          *     documents.
207          */
208         @NonNull
build()209         public PutDocumentsRequest build() {
210             mBuilt = true;
211 
212             // Verify there is no id collision between normal documents and action documents.
213             Set<String> idSet = new ArraySet<>();
214             for (int i = 0; i < mDocuments.size(); i++) {
215                 idSet.add(mDocuments.get(i).getId());
216             }
217             for (int i = 0; i < mTakenActions.size(); i++) {
218                 GenericDocument takenAction = mTakenActions.get(i);
219                 if (idSet.contains(takenAction.getId())) {
220                     throw new IllegalArgumentException(
221                             "Document id "
222                                     + takenAction.getId()
223                                     + " cannot exist in both taken action and normal document");
224                 }
225             }
226 
227             return new PutDocumentsRequest(mDocuments, mTakenActions);
228         }
229 
resetIfBuilt()230         private void resetIfBuilt() {
231             if (mBuilt) {
232                 mDocuments = new ArrayList<>(mDocuments);
233                 mTakenActions = new ArrayList<>(mTakenActions);
234                 mBuilt = false;
235             }
236         }
237     }
238 }
239