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