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 androidx.appsearch.app; 18 19 import android.annotation.SuppressLint; 20 21 import androidx.annotation.NonNull; 22 import androidx.annotation.RestrictTo; 23 import androidx.appsearch.annotation.CanIgnoreReturnValue; 24 import androidx.appsearch.exceptions.AppSearchException; 25 import androidx.appsearch.flags.FlaggedApi; 26 import androidx.appsearch.flags.Flags; 27 import androidx.appsearch.usagereporting.TakenAction; 28 import androidx.collection.ArraySet; 29 import androidx.core.util.Preconditions; 30 31 import java.util.ArrayList; 32 import java.util.Arrays; 33 import java.util.Collection; 34 import java.util.Collections; 35 import java.util.List; 36 import java.util.Set; 37 38 /** 39 * Encapsulates a request to index documents into an {@link AppSearchSession} database. 40 * 41 * <!--@exportToFramework:ifJetpack()--> 42 * <p>Documents added to the request can be instances of classes annotated with 43 * {@link androidx.appsearch.annotation.Document} or instances of 44 * {@link GenericDocument}. 45 * <!--@exportToFramework:else()--> 46 * 47 * @see AppSearchSession#putAsync 48 */ 49 // TODO(b/384721898): Switch to JSpecify annotations 50 @SuppressWarnings("JSpecifyNullness") 51 public final class PutDocumentsRequest { 52 private final List<GenericDocument> mDocuments; 53 54 private final List<GenericDocument> mTakenActions; 55 PutDocumentsRequest(List<GenericDocument> documents, List<GenericDocument> takenActions)56 PutDocumentsRequest(List<GenericDocument> documents, List<GenericDocument> takenActions) { 57 mDocuments = documents; 58 mTakenActions = takenActions; 59 } 60 61 /** Returns a list of {@link GenericDocument} objects that are part of this request. */ getGenericDocuments()62 public @NonNull List<GenericDocument> getGenericDocuments() { 63 return Collections.unmodifiableList(mDocuments); 64 } 65 66 /** 67 * Returns a list of {@link GenericDocument} objects containing taken action metrics that are 68 * part of this request. 69 * 70 * <!--@exportToFramework:ifJetpack()--> 71 * <p>See {@link Builder#addTakenActions(TakenAction...)}. 72 * <!--@exportToFramework:else() 73 * <p>See {@link Builder#addTakenActionGenericDocuments(GenericDocument...)}. 74 * --> 75 */ 76 @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS) getTakenActionGenericDocuments()77 public @NonNull List<GenericDocument> getTakenActionGenericDocuments() { 78 return Collections.unmodifiableList(mTakenActions); 79 } 80 81 82 /** Builder for {@link PutDocumentsRequest} objects. */ 83 public static final class Builder { 84 private ArrayList<GenericDocument> mDocuments = new ArrayList<>(); 85 private ArrayList<GenericDocument> mTakenActions = new ArrayList<>(); 86 private boolean mBuilt = false; 87 88 /** Adds one or more {@link GenericDocument} objects to the request. */ 89 @CanIgnoreReturnValue addGenericDocuments(@onNull GenericDocument... documents)90 public @NonNull Builder addGenericDocuments(@NonNull GenericDocument... documents) { 91 Preconditions.checkNotNull(documents); 92 resetIfBuilt(); 93 return addGenericDocuments(Arrays.asList(documents)); 94 } 95 96 /** Adds a collection of {@link GenericDocument} objects to the request. */ 97 @CanIgnoreReturnValue addGenericDocuments( @onNull Collection<? extends GenericDocument> documents)98 public @NonNull Builder addGenericDocuments( 99 @NonNull Collection<? extends GenericDocument> documents) { 100 Preconditions.checkNotNull(documents); 101 resetIfBuilt(); 102 mDocuments.addAll(documents); 103 return this; 104 } 105 106 // @exportToFramework:startStrip() 107 /** 108 * Adds one or more annotated {@link androidx.appsearch.annotation.Document} 109 * documents to the request. 110 * 111 * @param documents annotated 112 * {@link androidx.appsearch.annotation.Document} documents. 113 * @throws AppSearchException if an error occurs converting a document class into a 114 * {@link GenericDocument}. 115 */ 116 // Merged list available from getGenericDocuments() 117 @SuppressLint("MissingGetterMatchingBuilder") 118 @CanIgnoreReturnValue addDocuments(@onNull Object... documents)119 public @NonNull Builder addDocuments(@NonNull Object... documents) 120 throws AppSearchException { 121 Preconditions.checkNotNull(documents); 122 resetIfBuilt(); 123 return addDocuments(Arrays.asList(documents)); 124 } 125 126 /** 127 * Adds a collection of annotated 128 * {@link androidx.appsearch.annotation.Document} documents to the request. 129 * 130 * @param documents annotated 131 * {@link androidx.appsearch.annotation.Document} documents. 132 * @throws AppSearchException if an error occurs converting a document into a 133 * {@link GenericDocument}. 134 */ 135 // Merged list available from getGenericDocuments() 136 @SuppressLint("MissingGetterMatchingBuilder") 137 @CanIgnoreReturnValue addDocuments(@onNull Collection<?> documents)138 public @NonNull Builder addDocuments(@NonNull Collection<?> documents) 139 throws AppSearchException { 140 Preconditions.checkNotNull(documents); 141 resetIfBuilt(); 142 List<GenericDocument> genericDocuments = new ArrayList<>(documents.size()); 143 for (Object document : documents) { 144 GenericDocument genericDocument = GenericDocument.fromDocumentClass(document); 145 genericDocuments.add(genericDocument); 146 } 147 return addGenericDocuments(genericDocuments); 148 } 149 150 /** 151 * Adds one or more {@link TakenAction} objects to the request. 152 * 153 * <p>Clients can construct {@link TakenAction} documents to report the user's actions on 154 * search results, and these actions can be used as signals to boost result ranking in 155 * future search requests. See {@link TakenAction} for more details. 156 * 157 * <p>Clients should report search and click actions together sorted by 158 * {@link TakenAction#getActionTimestampMillis} in chronological order. 159 * <p>For example, if there are 2 search actions, with 1 click action associated with the 160 * first and 2 click actions associated with the second, then clients should report 161 * [searchAction1, clickAction1, searchAction2, clickAction2, clickAction3]. 162 * 163 * <p>Certain anonymized information about actions reported using this API may be uploaded 164 * using statsd and may be used to improve the quality of the search algorithms. Most of 165 * the information in this class is already non-identifiable, such as durations and its 166 * position in the result set. Identifiable information which you choose to provide, such 167 * as the query string, will be anonymized using techniques like Federated Analytics to 168 * ensure only the most frequently searched terms across the whole user population are 169 * retained and available for study. 170 * 171 * <p>You can alternatively use the {@link #addDocuments(Object...)} API with 172 * {@link TakenAction} document to retain the benefits of joining and using it on-device, 173 * without triggering any of the anonymized stats uploading described above. 174 * 175 * @param takenActions one or more {@link TakenAction} objects. 176 */ 177 // Merged list available from getTakenActionGenericDocuments() 178 @SuppressWarnings("MissingGetterMatchingBuilder") 179 @CanIgnoreReturnValue 180 @ExperimentalAppSearchApi addTakenActions( @onNull TakenAction... takenActions)181 public @NonNull Builder addTakenActions( 182 @NonNull TakenAction... takenActions) throws AppSearchException { 183 Preconditions.checkNotNull(takenActions); 184 resetIfBuilt(); 185 return addTakenActions(Arrays.asList(takenActions)); 186 } 187 188 /** 189 * Adds a collection of {@link TakenAction} objects to the request. 190 * 191 * @see #addTakenActions(TakenAction...) 192 * 193 * @param takenActions a collection of {@link TakenAction} objects. 194 */ 195 // Merged list available from getTakenActionGenericDocuments() 196 @SuppressWarnings("MissingGetterMatchingBuilder") 197 @CanIgnoreReturnValue 198 @ExperimentalAppSearchApi addTakenActions( @onNull Collection<? extends TakenAction> takenActions)199 public @NonNull Builder addTakenActions( 200 @NonNull Collection<? extends TakenAction> takenActions) 201 throws AppSearchException { 202 Preconditions.checkNotNull(takenActions); 203 resetIfBuilt(); 204 List<GenericDocument> genericDocuments = new ArrayList<>(takenActions.size()); 205 for (Object takenAction : takenActions) { 206 GenericDocument genericDocument = GenericDocument.fromDocumentClass(takenAction); 207 genericDocuments.add(genericDocument); 208 } 209 mTakenActions.addAll(genericDocuments); 210 return this; 211 } 212 // @exportToFramework:endStrip() 213 214 /** 215 * Adds one or more {@link GenericDocument} objects containing taken action metrics to the 216 * request. 217 * 218 * <p>It is recommended to use taken action document classes in Jetpack library to construct 219 * taken action documents. 220 * 221 * <p>The document creation timestamp of the {@link GenericDocument} should be set to the 222 * actual action timestamp via {@link GenericDocument.Builder#setCreationTimestampMillis}. 223 * 224 * <p>Clients should report search and click actions together sorted by 225 * {@link GenericDocument#getCreationTimestampMillis} in chronological order. 226 * <p>For example, if there are 2 search actions, with 1 click action associated with the 227 * first and 2 click actions associated with the second, then clients should report 228 * [searchAction1, clickAction1, searchAction2, clickAction2, clickAction3]. 229 * 230 * <p>Different types of taken actions and metrics to be collected by AppSearch: 231 * <ul> 232 * <li> 233 * Search action 234 * <ul> 235 * <li>actionType: LONG, the enum value of the action type. 236 * <p>Requires to be {@code 1} for search actions. 237 * 238 * <li>query: STRING, the user-entered search input (without any operators or 239 * rewriting). 240 * 241 * <li>fetchedResultCount: LONG, the number of {@link SearchResult} documents 242 * fetched from AppSearch in this search action. 243 * </ul> 244 * </li> 245 * 246 * <li> 247 * Click action 248 * <ul> 249 * <li>actionType: LONG, the enum value of the action type. 250 * <p>Requires to be {@code 2} for click actions. 251 * 252 * <li>query: STRING, the user-entered search input (without any operators or 253 * rewriting) that yielded the {@link SearchResult} on which the user took action. 254 * 255 * <li>referencedQualifiedId: STRING, the qualified id of the {@link SearchResult} 256 * document that the user takes action on. 257 * <p>A qualified id is a string generated by package, database, namespace, and 258 * document id. See 259 * {@link androidx.appsearch.util.DocumentIdUtil#createQualifiedId} for more 260 * details. 261 * 262 * <li>resultRankInBlock: LONG, the rank of the {@link SearchResult} document among 263 * the user-defined block. 264 * <p>The client can define its own custom definition for block, for example, 265 * corpus name, group, etc. 266 * <p>For example, a client defines the block as corpus, and AppSearch returns 5 267 * documents with corpus = ["corpus1", "corpus1", "corpus2", "corpus3", "corpus2"]. 268 * Then the block ranks of them = [1, 2, 1, 1, 2]. 269 * <p>If the client is not presenting the results in multiple blocks, they should 270 * set this value to match resultRankGlobal. 271 * 272 * <li>resultRankGlobal: LONG, the global rank of the {@link SearchResult} 273 * document. 274 * <p>Global rank reflects the order of {@link SearchResult} documents returned by 275 * AppSearch. 276 * <p>For example, AppSearch returns 2 pages with 10 {@link SearchResult} documents 277 * for each page. Then the global ranks of them will be 1 to 10 for the first page, 278 * and 11 to 20 for the second page. 279 * 280 * <li>timeStayOnResultMillis: LONG, the time in milliseconds that user stays on 281 * the {@link SearchResult} document after clicking it. 282 * </ul> 283 * </li> 284 * </ul> 285 * 286 * <p>Certain anonymized information about actions reported using this API may be uploaded 287 * using statsd and may be used to improve the quality of the search algorithms. Most of 288 * the information in this class is already non-identifiable, such as durations and its 289 * position in the result set. Identifiable information which you choose to provide, such 290 * as the query string, will be anonymized using techniques like Federated Analytics to 291 * ensure only the most frequently searched terms across the whole user population are 292 * retained and available for study. 293 * 294 * <p>You can alternatively use the {@link #addGenericDocuments(GenericDocument...)} API to 295 * retain the benefits of joining and using it on-device, without triggering any of the 296 * anonymized stats uploading described above. 297 * 298 * @param takenActionGenericDocuments one or more {@link GenericDocument} objects containing 299 * taken action metric fields. 300 */ 301 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 302 @CanIgnoreReturnValue 303 @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS) addTakenActionGenericDocuments( @onNull GenericDocument... takenActionGenericDocuments)304 public @NonNull Builder addTakenActionGenericDocuments( 305 @NonNull GenericDocument... takenActionGenericDocuments) 306 throws AppSearchException { 307 Preconditions.checkNotNull(takenActionGenericDocuments); 308 resetIfBuilt(); 309 return addTakenActionGenericDocuments(Arrays.asList(takenActionGenericDocuments)); 310 } 311 312 /** 313 * Adds a collection of {@link GenericDocument} objects containing taken action metrics to 314 * the request. 315 * 316 * @see #addTakenActionGenericDocuments(GenericDocument...) 317 * 318 * @param takenActionGenericDocuments a collection of {@link GenericDocument} objects 319 * containing taken action metric fields. 320 */ 321 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) 322 @CanIgnoreReturnValue 323 @FlaggedApi(Flags.FLAG_ENABLE_PUT_DOCUMENTS_REQUEST_ADD_TAKEN_ACTIONS) addTakenActionGenericDocuments(@onNull Collection<? extends GenericDocument> takenActionGenericDocuments)324 public @NonNull Builder addTakenActionGenericDocuments(@NonNull Collection<? 325 extends GenericDocument> takenActionGenericDocuments) throws AppSearchException { 326 Preconditions.checkNotNull(takenActionGenericDocuments); 327 resetIfBuilt(); 328 mTakenActions.addAll(takenActionGenericDocuments); 329 return this; 330 } 331 332 /** 333 * Creates a new {@link PutDocumentsRequest} object. 334 * 335 * @throws IllegalArgumentException if there is any id collision between normal and action 336 * documents. 337 */ build()338 public @NonNull PutDocumentsRequest build() { 339 mBuilt = true; 340 341 // Verify there is no id collision between normal documents and action documents. 342 Set<String> idSet = new ArraySet<>(); 343 for (int i = 0; i < mDocuments.size(); i++) { 344 idSet.add(mDocuments.get(i).getId()); 345 } 346 for (int i = 0; i < mTakenActions.size(); i++) { 347 GenericDocument takenAction = mTakenActions.get(i); 348 if (idSet.contains(takenAction.getId())) { 349 throw new IllegalArgumentException("Document id " + takenAction.getId() 350 + " cannot exist in both taken action and normal document"); 351 } 352 } 353 354 return new PutDocumentsRequest(mDocuments, mTakenActions); 355 } 356 resetIfBuilt()357 private void resetIfBuilt() { 358 if (mBuilt) { 359 mDocuments = new ArrayList<>(mDocuments); 360 mTakenActions = new ArrayList<>(mTakenActions); 361 mBuilt = false; 362 } 363 } 364 } 365 } 366